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

Lesson 4: Directives, Rendering & Template Refs

v-if, v-for, v-show, v-bind, v-on. Custom directives. Template refs, nextTick. Render functions, JSX. Teleport, Suspense, KeepAlive. Dynamic/async components.

💻 Programming — Lesson 4 Lesson 4: Directives, Rendering & Templates Refs

Vue.js & Nuxt: From Basics to Advanced

Part 1: Vue.js Fundamentals

xdev.asia

1. Built-in Directives

<template>
  <!-- v-show vs v-if -->
  <div v-show="isVisible">Toggle display (CSS)</div>
  <div v-if="isLoggedIn">Welcome!</div>
  <div v-else>Please login</div>

  <!-- v-for with key -->
  <div v-for="(item, index) in items" :key="item.id">
    {{ index }}: {{ item.name }}
  </div>

  <!-- v-bind shorthand -->
  <div :class="{ active: isActive, 'text-bold': isBold }">...</div>
  <div :style="{ color: textColor, fontSize: size + 'px' }">...</div>
  <component v-bind="dynamicProps" />

  <!-- v-on modifiers -->
  <form @submit.prevent="handleSubmit">...</form>
  <button @click.stop="handleClick">Stop propagation</button>
  <input @keydown.enter.exact="submit" />
  <div @click.self="handleSelf">Only direct clicks</div>
</template>

2. Custom Directives

// directives/vFocus.ts
import type { Directive } from 'vue'

export const vFocus: Directive = {
  mounted(el: HTMLElement) {
    el.focus()
  },
}

// directives/vClickOutside.ts
export const vClickOutside: Directive = {
  mounted(el, binding) {
    el._clickOutside = (event: Event) => {
      if (!el.contains(event.target as Node)) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutside)
  },
}
<script setup lang="ts">
import { vFocus } from '@/directives/vFocus'
import { vClickOutside } from '@/directives/vClickOutside'

function closeDropdown() {
  isOpen.value = false
}
</script>

<template>
  <input v-focus placeholder="Auto focused" />
  <div v-click-outside="closeDropdown">Dropdown content</div>
</template>

3. Template Refs

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

const inputRef = ref<HTMLInputElement | null>(null)
const containerRef = ref<HTMLDivElement | null>(null)

onMounted(() => {
  inputRef.value?.focus()
})

async function addItem() {
  items.value.push(newItem)
  // DOM chưa update — cần nextTick
  await nextTick()
  containerRef.value?.scrollTo({ top: containerRef.value.scrollHeight })
}
</script>

<template>
  <input ref="inputRef" />
  <div ref="containerRef" class="scroll-container">...</div>
</template>

4. Teleportation

<!-- Modal.vue — render outside component tree -->
<script setup lang="ts">
defineProps<{ isOpen: boolean }>()
defineEmits<{ (e: 'close'): void }>()
</script>

<template>
  <Teleport to="body">
    <div v-if="isOpen" class="modal-overlay" @click.self="$emit('close')">
      <div class="modal-content">
        <slot />
        <button @click="$emit('close')">Đóng</button>
      </div>
    </div>
  </Teleport>
</template>

5. KeepAlive & Dynamic Components

<script setup lang="ts">
import { shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'

const currentTab = shallowRef(TabA)
const tabs = [
  { label: 'Tab A', component: TabA },
  { label: 'Tab B', component: TabB },
  { label: 'Tab C', component: TabC },
]
</script>

<template>
  <div class="tabs">
    <button
      v-for="tab in tabs"
      :key="tab.label"
      @click="currentTab = tab.component"
    >
      {{ tab.label }}
    </button>
  </div>

  <!-- KeepAlive caches component state -->
  <KeepAlive :max="5">
    <component :is="currentTab" />
  </KeepAlive>
</template>

6. Suspense

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

Next article: Vue Router Deep Dive — routing, guards, lazy loading.