Chuyển đến nội dung chính

Lesson 3: Components, Props & Events

Component registration, props validation. v-model, custom events, emit. Slots (default, named, scoped). Provide/inject, component lifecycle hooks.

💻 Programming — Lesson 3 Lesson 3: Components, Props & Events

Vue.js & Nuxt: From Basics to Advanced

Part 1: Vue.js Fundamentals

xdev.asia

1. Component Registration

<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
  name: string
  email: string
  avatar?: string
  role?: 'admin' | 'user' | 'editor'
}

const props = withDefaults(defineProps<Props>(), {
  role: 'user',
})
</script>

<template>
  <div class="user-card">
    <img :src="props.avatar || '/default-avatar.png'" :alt="props.name" />
    <h3>{{ props.name }}</h3>
    <p>{{ props.email }}</p>
    <span class="badge">{{ props.role }}</span>
  </div>
</template>
<!-- Parent.vue — sử dụng component -->
<script setup lang="ts">
import UserCard from './UserCard.vue'
</script>

<template>
  <UserCard name="Duy Tran" email="[email protected]" role="admin" />
</template>

2. v-model & Custom Events

<!-- SearchInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()
// Vue 3.4+ — defineModel replaces props + emit pattern

const emit = defineEmits<{
  (e: 'search', query: string): void
}>()

function handleSearch() {
  if (model.value) {
    emit('search', model.value)
  }
}
</script>

<template>
  <div class="search">
    <input v-model="model" placeholder="Tìm kiếm..." @keydown.enter="handleSearch" />
    <button @click="handleSearch">🔍</button>
  </div>
</template>

<!-- Parent -->
<script setup lang="ts">
import { ref } from 'vue'
const query = ref('')
</script>

<template>
  <SearchInput v-model="query" @search="handleSearch" />
</template>

3. Slots

<!-- Card.vue -->
<template>
  <div class="card">
    <!-- Named slots -->
    <div class="card-header">
      <slot name="header">Default Header</slot>
    </div>

    <div class="card-body">
      <slot>Default content</slot>
    </div>

    <div class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

<!-- Usage -->
<Card>
  <template #header>
    <h2>Tiêu đề</h2>
  </template>

  <p>Nội dung chính</p>

  <template #footer>
    <button>Submit</button>
  </template>
</Card>

4. Scoped Slots

<!-- DataTable.vue -->
<script setup lang="ts" generic="T">
defineProps<{
  items: T[]
  columns: { key: keyof T; label: string }[]
}>()
</script>

<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="String(col.key)">{{ col.label }}</th>
        <th>Actions</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in items" :key="index">
        <td v-for="col in columns" :key="String(col.key)">
          <slot :name="String(col.key)" :value="item[col.key]" :item="item">
            {{ item[col.key] }}
          </slot>
        </td>
        <td>
          <slot name="actions" :item="item" />
        </td>
      </tr>
    </tbody>
  </table>
</template>

5. Provide / Inject

<!-- Parent — provide -->
<script setup lang="ts">
import { provide, ref } from 'vue'

const theme = ref<'light' | 'dark'>('light')
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

provide('theme', { theme, toggleTheme })
</script>

<!-- Deep child — inject -->
<script setup lang="ts">
import { inject } from 'vue'

const { theme, toggleTheme } = inject('theme')!
</script>

<template>
  <button @click="toggleTheme">
    Current: {{ theme }}
  </button>
</template>

6. Lifecycle Hooks

<script setup lang="ts">
import { onMounted, onUnmounted, onUpdated } from 'vue'

onMounted(() => {
  console.log('Component mounted — DOM ready')
  window.addEventListener('resize', handleResize)
})

onUpdated(() => {
  console.log('Component updated')
})

onUnmounted(() => {
  console.log('Component unmounted — cleanup')
  window.removeEventListener('resize', handleResize)
})
</script>

Next article: Directives, Rendering & Template Refs — custom directives, render functions, Teleport.