Files
gh-alexanderop-claude-skill…/skills/vue-development/references/component-patterns.md
2025-11-29 17:52:04 +08:00

5.1 KiB

Vue Component Patterns Reference

Table of Contents

Props Definition

<script setup lang="ts">
// ✅ CORRECT: TypeScript generics, no const (props not used in script)
defineProps<{
  userId: number
  userName: string
  isActive?: boolean
}>()

// ✅ CORRECT: With const ONLY if props used in script
const props = defineProps<{
  count: number
}>()
console.log(props.count) // Used in script

// ✅ CORRECT: With defaults (requires destructuring)
const { theme = 'light', size = 'medium' } = defineProps<{
  theme?: string
  size?: string
}>()

// ❌ WRONG: Runtime validation when TypeScript available
const props = defineProps({
  user: {
    type: Object as PropType<User>,
    required: true
  }
})

// ❌ WRONG: const when props not used
const props = defineProps<{ count: number }>()
// props never referenced in script
</script>

Rules:

  • ALWAYS use TypeScript generics: defineProps<{ }>()
  • Use const props = ONLY if props referenced in script block
  • NEVER use runtime validation (type, required, PropType) when TypeScript available
  • Destructure ONLY for default values

Emits Definition

<script setup lang="ts">
// ✅ CORRECT: TypeScript with event payloads
const emit = defineEmits<{
  submit: [formData: FormData]
  cancel: []
  update: [userId: number, changes: Partial<User>]
}>()

emit('submit', formData)
emit('cancel')
emit('update', 123, { name: 'Updated' })

// ❌ WRONG: Array syntax without types
const emit = defineEmits(['submit', 'cancel'])

// ❌ WRONG: Runtime validation
const emit = defineEmits({
  submit: (payload: FormData) => true
})
</script>

Rules:

  • ALWAYS use const emit = defineEmits<{ }>()
  • ALWAYS define event payload types: eventName: [arg1: type, arg2: type] or eventName: []
  • ALWAYS use camelCase in script, kebab-case in templates
  • Use semantic event names: submit, delete, userUpdated (not generic update)

V-Model Bindings

<script setup lang="ts">
// ✅ CORRECT: Simple v-model
const title = defineModel<string>({ required: true })

// ✅ CORRECT: With options
const [count, modifiers] = defineModel<number>({
  default: 0,
  get: (value) => Math.max(0, value), // transform on read
  set: (value) => {
    if (modifiers.even) {
      return Math.round(value / 2) * 2
    }
    return value
  }
})

// ✅ CORRECT: Multiple v-models
const firstName = defineModel<string>('firstName')
const age = defineModel<number>('age')
// Usage: <UserForm v-model:first-name="user.firstName" v-model:age="user.age" />

// ❌ WRONG: Manual modelValue implementation
const props = defineProps<{
  modelValue: string
}>()
const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()
// Should use: const modelValue = defineModel<string>()
</script>

Rules:

  • ALWAYS use defineModel<type>() for v-model bindings
  • NEVER manually define modelValue prop + update:modelValue emit
  • Use named models for multiple bindings: defineModel<type>('name')
  • defineModel handles all prop/emit wiring automatically

Template Patterns

<template>
  <!--  CORRECT: Prop shorthand -->
  <UserCard :user-id />

  <!--  WRONG: Verbose when value matches prop name -->
  <UserCard :user-id="userId" />

  <!--  CORRECT: Explicit template tags with shorthand slots -->
  <Card>
    <template #header>
      <h2>Title</h2>
    </template>
    <template #default>
      <p>Content</p>
    </template>
  </Card>

  <!--  WRONG: v-slot longhand -->
  <Card>
    <template v-slot:header>...</template>
  </Card>

  <!--  WRONG: Implicit default slot (missing template tag) -->
  <Card>
    <p>Content</p>
  </Card>
</template>

Rules:

  • Use :propName when value variable matches prop name (instead of :propName="propName")
  • ALWAYS use explicit <template> tags for ALL slots
  • ALWAYS use shorthand: #slotName instead of v-slot:slotName
  • Use kebab-case for props/events in templates, camelCase in script

Component Naming

✅ CORRECT:
src/components/
  UserProfile.vue          (PascalCase)
  SearchButtonClear.vue    (General → Specific)

✅ ALSO CORRECT:
src/components/
  user-profile.vue         (kebab-case)
  search-button-clear.vue

❌ WRONG:
ClearSearchButton.vue      (Specific → General)
userProfile.vue            (camelCase)

Rules:

  • File names: PascalCase OR kebab-case (be consistent in project)
  • Component names in script: ALWAYS PascalCase
  • Compose names from general to specific: SearchButtonClear not ClearSearchButton

V-Model Modifiers

For custom modifiers, fetch the official docs: https://vuejs.org/guide/components/v-model.md#handling-v-model-modifiers

Basic pattern:

<script setup lang="ts">
const [modelValue, modifiers] = defineModel<string>({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>