This commit is contained in:
sexygoat
2026-03-31 18:41:52 +08:00
commit 3c62cc1cd1
527 changed files with 96725 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/vue`
- **Git SHA:** `dcf363038ab2f5b4571028669cf893d084b6b207`
- **Generated:** 2026-01-28

View File

@@ -0,0 +1,63 @@
---
name: vue
description: Vue.js progressive JavaScript framework. Use when building Vue components, working with reactivity (ref, reactive, computed, watch), or implementing Vue Composition API patterns.
metadata:
author: Anthony Fu
version: "2026.1.28"
source: Generated from https://github.com/vuejs/docs, scripts located at https://github.com/antfu/skills
---
# Vue
> The skill is based on Vue 3.5+, generated at 2026-01-28.
Vue is a progressive JavaScript framework for building user interfaces. It builds on standard HTML, CSS, and JavaScript with intuitive API and world-class documentation. The Composition API with `<script setup>` and TypeScript is the recommended approach for building Vue applications.
## Core References
| Topic | Description | Reference |
|-------|-------------|-----------|
| Reactivity System | ref, reactive, computed, watch, and watchEffect | [core-reactivity](references/core-reactivity.md) |
## Components
| Topic | Description | Reference |
|-------|-------------|-----------|
| Props | Declare and validate component props with TypeScript | [components-props](references/components-props.md) |
| Events (Emits) | Emit custom events from components | [components-emits](references/components-emits.md) |
| Slots | Pass template content to child components | [components-slots](references/components-slots.md) |
| v-model | Two-way binding on custom components | [components-v-model](references/components-v-model.md) |
| Lifecycle Hooks | Run code at specific component lifecycle stages | [components-lifecycle](references/components-lifecycle.md) |
## Features
### Script Setup & TypeScript
| Topic | Description | Reference |
|-------|-------------|-----------|
| Script Setup | Composition API syntactic sugar for SFCs | [features-script-setup](references/features-script-setup.md) |
| TypeScript | Type-safe Vue components with Composition API | [features-typescript](references/features-typescript.md) |
### Reusability
| Topic | Description | Reference |
|-------|-------------|-----------|
| Composables | Encapsulate and reuse stateful logic | [features-composables](references/features-composables.md) |
| Custom Directives | Low-level DOM manipulation directives | [features-directives](references/features-directives.md) |
| Template Refs | Direct DOM and component instance access | [features-template-refs](references/features-template-refs.md) |
## Advanced
| Topic | Description | Reference |
|-------|-------------|-----------|
| Provide/Inject | Dependency injection across component tree | [advanced-provide-inject](references/advanced-provide-inject.md) |
| Async & Suspense | Top-level await pitfalls, async components, Suspense | [advanced-async-suspense](references/advanced-async-suspense.md) |
## Key Recommendations
- **Use `<script setup lang="ts">`** for all components
- **Prefer `ref()` over `reactive()`** for declaring state
- **Use type-based prop declarations** with interfaces
- **Use `defineModel()`** for v-model (3.4+)
- **Destructure props reactively** (3.5+) for cleaner code
- **Extract composables** for reusable stateful logic

View File

@@ -0,0 +1,246 @@
---
name: async-components-suspense
description: Handle async operations, top-level await, and Suspense boundaries
---
# Async Components & Suspense
Vue provides patterns for handling asynchronous operations in components.
## Top-Level await in Script Setup
`<script setup>` supports top-level `await`, making the component an async dependency:
```vue
<script setup lang="ts">
const data = await fetch('/api/data').then(r => r.json())
</script>
<template>
<div>{{ data }}</div>
</template>
```
**Important**: Components with top-level `await` **require a `<Suspense>` boundary** in a parent component, otherwise they won't render.
## Suspense
`<Suspense>` is a built-in component for handling async dependencies in the component tree.
```vue
<template>
<Suspense>
<!-- Component with async setup -->
<AsyncComponent />
<!-- Fallback while loading -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
```
### Suspense Slots
- **default**: The async content to render
- **fallback**: Content shown while async dependencies are resolving
```vue
<Suspense>
<template #default>
<Dashboard />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
```
## Async Components
Define components that are loaded asynchronously:
```ts
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal.vue')
)
// With options
const AsyncModalWithOptions = defineAsyncComponent({
loader: () => import('./components/Modal.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200, // Delay before showing loading (ms)
timeout: 3000 // Timeout before showing error (ms)
})
```
## Common Async Pitfalls
### Pitfall 1: Missing Suspense Boundary
```vue
<!-- Won't work - no Suspense boundary -->
<template>
<AsyncComponent />
</template>
<!-- Works -->
<template>
<Suspense>
<AsyncComponent />
<template #fallback>Loading...</template>
</Suspense>
</template>
```
### Pitfall 2: Lifecycle Hooks After await
Lifecycle hooks must be registered synchronously, before any `await`:
```vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
// ✅ Register hooks BEFORE await
onMounted(() => console.log('mounted'))
onUnmounted(() => console.log('unmounted'))
// Now you can await
const data = await fetchData()
</script>
```
```vue
<script setup lang="ts">
// ❌ WRONG - hooks after await won't work
const data = await fetchData()
onMounted(() => {
// This may not be called!
})
</script>
```
### Pitfall 3: Composables After await
Composables that use lifecycle hooks must be called before `await`:
```vue
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
// ✅ Call composables BEFORE await
const { x, y } = useMouse()
const data = await fetchData()
</script>
```
### Pitfall 4: Watchers Created in Async Callbacks
Watchers in async callbacks aren't auto-disposed:
```ts
// ❌ Memory leak - watcher not auto-disposed
setTimeout(() => {
watch(source, callback) // Must be stopped manually
}, 1000)
// ✅ Watchers in setup are auto-disposed
watch(source, callback)
```
## Recommended Pattern: Avoid Top-Level await
Often better to handle async in `onMounted` or with watchers:
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const data = ref<Data | null>(null)
const isLoading = ref(true)
const error = ref<Error | null>(null)
onMounted(async () => {
try {
data.value = await fetchData()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
})
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else>{{ data }}</div>
</template>
```
This pattern:
- Doesn't require Suspense
- Gives you control over loading/error states
- Works everywhere without special parent setup
## When to Use Suspense
Use Suspense when:
- You have nested async components
- You want coordinated loading states across multiple async children
- You're doing SSR with async data requirements
Avoid Suspense when:
- Simple single-component async loading
- You need fine-grained control over loading states
- You want to avoid Suspense complexity
## Suspense Events
```vue
<Suspense
@pending="onPending"
@resolve="onResolve"
@fallback="onFallback"
>
<AsyncComponent />
</Suspense>
```
## Error Handling with Suspense
Use `onErrorCaptured` or `<ErrorBoundary>` pattern:
```vue
<script setup lang="ts">
import { onErrorCaptured, ref } from 'vue'
const error = ref<Error | null>(null)
onErrorCaptured((e) => {
error.value = e
return false // Prevent propagation
})
</script>
<template>
<div v-if="error">Error: {{ error.message }}</div>
<Suspense v-else>
<AsyncComponent />
<template #fallback>Loading...</template>
</Suspense>
</template>
```
<!--
Source references:
- https://vuejs.org/guide/built-ins/suspense.html
- https://vuejs.org/guide/components/async.html
- https://vuejs.org/api/sfc-script-setup.html#top-level-await
-->

View File

@@ -0,0 +1,174 @@
---
name: provide-inject
description: Pass data through component tree without prop drilling
---
# Provide / Inject
Provide data from ancestor components to any descendant, avoiding prop drilling.
## Basic Usage
```vue
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const message = ref('hello')
provide('message', message)
</script>
```
```vue
<!-- DeepChild.vue (any level deep) -->
<script setup lang="ts">
import { inject } from 'vue'
const message = inject('message')
</script>
```
## Typing with InjectionKey
Use `InjectionKey` for type safety between provider and injector:
```ts
// keys.ts
import type { InjectionKey, Ref } from 'vue'
export const messageKey = Symbol() as InjectionKey<Ref<string>>
export const countKey = Symbol() as InjectionKey<number>
```
```vue
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import { messageKey } from './keys'
const message = ref('hello')
provide(messageKey, message)
</script>
```
```vue
<!-- Injector.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { messageKey } from './keys'
const message = inject(messageKey) // Ref<string> | undefined
</script>
```
## Default Values
```ts
// Simple default
const value = inject('message', 'default value')
// Factory function (for expensive defaults)
const value = inject('key', () => new ExpensiveClass(), true)
// ^ treat as factory
```
## App-Level Provide
Available to all components:
```ts
// main.ts
import { createApp } from 'vue'
const app = createApp(App)
app.provide('globalConfig', { theme: 'dark' })
```
## Reactive Provide/Inject
Provide reactive values for automatic updates:
```vue
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const count = ref(0)
provide('count', count)
</script>
```
The injected value maintains reactivity connection.
## Mutations Best Practice
Keep mutations in the provider, expose update functions:
```vue
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref, readonly } from 'vue'
const location = ref('North Pole')
function updateLocation(newLocation: string) {
location.value = newLocation
}
provide('location', {
location: readonly(location), // Prevent direct mutation
updateLocation
})
</script>
```
```vue
<!-- Injector.vue -->
<script setup lang="ts">
import { inject } from 'vue'
const { location, updateLocation } = inject('location')!
</script>
<template>
<button @click="updateLocation('South Pole')">
{{ location }}
</button>
</template>
```
## Using Symbol Keys
Recommended for libraries and large apps to avoid collisions:
```ts
// keys.ts
export const myKey = Symbol('myKey')
// provider
provide(myKey, value)
// injector
inject(myKey)
```
## Type Helpers
```ts
// String key with explicit type
const foo = inject<string>('foo')
// ^? string | undefined
// With default (removes undefined)
const foo = inject<string>('foo', 'default')
// ^? string
// Force non-undefined (use when certain it's provided)
const foo = inject('foo') as string
```
<!--
Source references:
- https://vuejs.org/guide/components/provide-inject.html
- https://vuejs.org/guide/typescript/composition-api.html#typing-provide-inject
-->

View File

@@ -0,0 +1,139 @@
---
name: component-events-emits
description: Emit and handle custom events from child to parent components
---
# Component Events
Components emit custom events to communicate with parent components.
## Emitting Events
Use `$emit` in templates or `defineEmits` in script:
```vue
<script setup lang="ts">
const emit = defineEmits(['inFocus', 'submit'])
function handleClick() {
emit('submit')
}
</script>
<template>
<button @click="$emit('inFocus')">Focus</button>
<button @click="handleClick">Submit</button>
</template>
```
## Type-Based Declaration (Recommended)
```vue
<script setup lang="ts">
// Call signature syntax
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// Named tuple syntax (3.3+, more concise)
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
// Usage
emit('change', 123)
emit('update', 'hello')
</script>
```
## Event Arguments
Pass additional arguments to events:
```vue
<script setup lang="ts">
const emit = defineEmits<{
increaseBy: [amount: number]
}>()
function increase() {
emit('increaseBy', 5)
}
</script>
```
Parent receives arguments:
```vue-html
<!-- Inline handler -->
<MyButton @increase-by="(n) => count += n" />
<!-- Method handler -->
<MyButton @increase-by="increaseCount" />
```
```ts
function increaseCount(n: number) {
count.value += n
}
```
## Event Validation
Validate event payloads with object syntax:
```vue
<script setup lang="ts">
const emit = defineEmits({
// No validation
click: null,
// Validate submit event
submit: ({ email, password }: { email: string; password: string }) => {
if (email && password) {
return true
}
console.warn('Invalid submit payload!')
return false
}
})
</script>
```
## Listening to Events
Use `v-on` or `@` shorthand:
```vue-html
<MyComponent @some-event="handleEvent" />
<!-- With .once modifier -->
<MyComponent @some-event.once="handleOnce" />
```
## Event Name Casing
- Emit in camelCase
- Listen in kebab-case
```ts
emit('someEvent')
```
```vue-html
<MyComponent @some-event="handler" />
```
## Important Notes
- Component events do NOT bubble (unlike DOM events)
- Only direct child events can be listened to
- For sibling/deeply nested communication, use provide/inject or state management
<!--
Source references:
- https://vuejs.org/guide/components/events.html
- https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits
-->

View File

@@ -0,0 +1,192 @@
---
name: lifecycle-hooks
description: Run code at specific stages of component lifecycle
---
# Lifecycle Hooks
Execute code at specific stages of a component's lifecycle.
## Common Lifecycle Hooks
```vue
<script setup lang="ts">
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// Before DOM is mounted
onBeforeMount(() => {
console.log('before mount')
})
// After DOM is mounted (most common)
onMounted(() => {
console.log('mounted')
// Safe to access DOM, make API calls, start timers
})
// Before reactive state change causes re-render
onBeforeUpdate(() => {
console.log('before update')
})
// After DOM re-render
onUpdated(() => {
console.log('updated')
})
// Before component unmounts
onBeforeUnmount(() => {
console.log('before unmount')
})
// After component unmounts
onUnmounted(() => {
console.log('unmounted')
// Cleanup: remove listeners, cancel timers, etc.
})
</script>
```
## Lifecycle Diagram
```
Creation:
setup() → onBeforeMount → onMounted
Updates:
onBeforeUpdate → onUpdated
Destruction:
onBeforeUnmount → onUnmounted
```
## Common Patterns
### DOM Access
```ts
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
// DOM is available
inputRef.value?.focus()
})
```
### API Calls
```ts
import { ref, onMounted } from 'vue'
const data = ref(null)
onMounted(async () => {
const response = await fetch('/api/data')
data.value = await response.json()
})
```
### Cleanup
```ts
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
```
### Timer Cleanup
```ts
import { onMounted, onUnmounted } from 'vue'
let timer: ReturnType<typeof setInterval>
onMounted(() => {
timer = setInterval(() => {
// ...
}, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
```
## Less Common Hooks
```ts
import {
onActivated, // Component kept alive is activated
onDeactivated, // Component kept alive is deactivated
onErrorCaptured, // Error from descendant captured
onRenderTracked, // Debug: reactive dependency tracked
onRenderTriggered // Debug: re-render triggered
} from 'vue'
```
## SSR Hooks
```ts
import { onServerPrefetch } from 'vue'
// Only runs during server-side rendering
onServerPrefetch(async () => {
data.value = await fetchData()
})
```
## Important Notes
1. **Hooks must be called synchronously** during `setup()`:
```ts
// ❌ Won't work
setTimeout(() => {
onMounted(() => {})
}, 100)
// ✅ Works
onMounted(() => {
setTimeout(() => {}, 100)
})
```
2. **Can call from external functions** if called synchronously from setup:
```ts
// composable.ts
export function useFeature() {
onMounted(() => {
// This works if useFeature is called synchronously in setup
})
}
```
3. **Multiple calls are allowed** - all callbacks will be invoked:
```ts
onMounted(() => console.log('first'))
onMounted(() => console.log('second'))
// Both will run
```
<!--
Source references:
- https://vuejs.org/guide/essentials/lifecycle.html
- https://vuejs.org/api/composition-api-lifecycle.html
-->

View File

@@ -0,0 +1,211 @@
---
name: component-props
description: Declare and use props with type safety and validation
---
# Component Props
Props are custom attributes for passing data from parent to child components.
## Type-Based Declaration (Recommended)
Use TypeScript interfaces with `defineProps`:
```vue
<script setup lang="ts">
interface Props {
title: string
count?: number
items: string[]
}
const props = defineProps<Props>()
</script>
```
## Reactive Props Destructure (3.5+)
Destructure props while keeping reactivity:
```vue
<script setup lang="ts">
interface Props {
msg?: string
count?: number
}
const { msg = 'hello', count = 0 } = defineProps<Props>()
// Both msg and count are reactive
watchEffect(() => {
console.log(msg, count)
})
</script>
```
### Passing destructured props to functions
```ts
const { foo } = defineProps(['foo'])
// ❌ Won't work - passes value not reactive source
watch(foo, /* ... */)
// ✅ Wrap in getter
watch(() => foo, /* ... */)
// ✅ For composables
useComposable(() => foo)
```
## Default Values with withDefaults (3.4 and below)
```vue
<script setup lang="ts">
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two'] // Factory function for objects/arrays
})
</script>
```
## Runtime Declaration
For runtime validation without TypeScript:
```vue
<script setup lang="ts">
const props = defineProps({
// Basic type check
propA: Number,
// Multiple types
propB: [String, Number],
// Required
propC: { type: String, required: true },
// With default
propD: { type: Number, default: 100 },
// Object with factory default
propE: {
type: Object,
default: () => ({ message: 'hello' })
},
// Custom validator
propF: {
validator(value: string) {
return ['success', 'warning', 'danger'].includes(value)
}
}
})
</script>
```
## Prop Types
Supported `type` values:
- `String`, `Number`, `Boolean`, `Array`, `Object`
- `Date`, `Function`, `Symbol`, `Error`
- Custom classes (uses `instanceof`)
```ts
class Person {
constructor(public name: string) {}
}
defineProps({
author: Person // instanceof check
})
```
## Nullable Props
```ts
defineProps({
id: {
type: [String, null],
required: true
}
})
```
## Boolean Casting
Boolean props have special casting rules:
```vue
<script setup lang="ts">
defineProps({
disabled: Boolean
})
</script>
<!-- equivalent to :disabled="true" -->
<MyComponent disabled />
<!-- equivalent to :disabled="false" -->
<MyComponent />
```
## One-Way Data Flow
Props form a one-way binding: parent → child. Never mutate props directly.
```ts
const props = defineProps(['initialCounter'])
// ❌ Don't mutate props
props.initialCounter = 5
// ✅ Use local state initialized from prop
const counter = ref(props.initialCounter)
// ✅ Or use computed for transformations
const normalizedSize = computed(() => props.size.trim().toLowerCase())
```
## Passing Props
```vue-html
<!-- Static -->
<BlogPost title="My Post" />
<!-- Dynamic -->
<BlogPost :title="post.title" />
<!-- Number (requires v-bind) -->
<BlogPost :likes="42" />
<!-- Boolean -->
<BlogPost is-published />
<BlogPost :is-published="false" />
<!-- Object spread -->
<BlogPost v-bind="post" />
<!-- equivalent to -->
<BlogPost :id="post.id" :title="post.title" />
```
## Prop Name Casing
- Declare with camelCase in JavaScript
- Use kebab-case in templates
```ts
defineProps({
greetingMessage: String
})
```
```vue-html
<MyComponent greeting-message="hello" />
```
<!--
Source references:
- https://vuejs.org/guide/components/props.html
- https://vuejs.org/api/sfc-script-setup.html#defineprops-defineemits
-->

View File

@@ -0,0 +1,201 @@
---
name: component-slots
description: Pass template content to child components with default, named, and scoped slots
---
# Component Slots
Slots allow parent components to pass template content into child components.
## Basic Slot
Child component with slot outlet:
```vue
<!-- FancyButton.vue -->
<template>
<button class="fancy-btn">
<slot></slot> <!-- slot outlet -->
</button>
</template>
```
Parent passes slot content:
```vue-html
<FancyButton>
Click me!
</FancyButton>
```
Renders as:
```html
<button class="fancy-btn">Click me!</button>
```
## Fallback Content
Provide default content when no slot content is provided:
```vue
<template>
<button>
<slot>Submit</slot> <!-- "Submit" is fallback -->
</button>
</template>
```
## Named Slots
Use multiple slots with names:
```vue
<!-- BaseLayout.vue -->
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- default slot -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
```
Use `v-slot` or `#` shorthand to target named slots:
```vue-html
<BaseLayout>
<template #header>
<h1>Page Title</h1>
</template>
<p>Main content goes here</p>
<template #footer>
<p>Contact info</p>
</template>
</BaseLayout>
```
## Scoped Slots
Pass data from child to parent slot content:
```vue
<!-- MyComponent.vue -->
<template>
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
</template>
<script setup lang="ts">
const greetingMessage = 'hello'
</script>
```
Receive slot props in parent:
```vue-html
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
<!-- Or with destructuring -->
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
```
### Named Scoped Slots
```vue-html
<MyComponent>
<template #header="headerProps">
{{ headerProps.title }}
</template>
<template #default="{ message }">
{{ message }}
</template>
</MyComponent>
```
## Conditional Slots
Check if a slot has content using `$slots`:
```vue
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div v-if="$slots.default" class="card-content">
<slot />
</div>
</div>
</template>
```
## Dynamic Slot Names
```vue-html
<template #[dynamicSlotName]>
...
</template>
```
## Renderless Components
Components that only provide logic via scoped slots:
```vue
<!-- MouseTracker.vue -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(e: MouseEvent) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>
<slot :x="x" :y="y"></slot>
</template>
```
Usage:
```vue-html
<MouseTracker v-slot="{ x, y }">
Mouse: {{ x }}, {{ y }}
</MouseTracker>
```
> Note: Composables are often preferred over renderless components for pure logic reuse.
## Render Scope
- Slot content has access to parent scope
- Slot content does NOT have access to child scope
- Use scoped slots to expose child data
<!--
Source references:
- https://vuejs.org/guide/components/slots.html
-->

View File

@@ -0,0 +1,183 @@
---
name: component-v-model
description: Implement two-way data binding on custom components
---
# Component v-model
Create two-way bindings between parent and child components.
## defineModel() (3.4+, Recommended)
The simplest way to implement v-model:
```vue
<!-- Child.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input v-model="model" />
</template>
```
```vue-html
<!-- Parent.vue -->
<Child v-model="searchText" />
```
`defineModel()` returns a ref that:
- Syncs with parent's bound value
- Emits `update:modelValue` when mutated
### With Options
```ts
// Required model
const model = defineModel<string>({ required: true })
// With default
const model = defineModel<number>({ default: 0 })
```
## Named v-model
Use arguments for multiple v-models:
```vue
<!-- UserName.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
</script>
<template>
<input v-model="firstName" />
<input v-model="lastName" />
</template>
```
```vue-html
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
```
## v-model Modifiers
Access and handle modifiers:
```vue-html
<MyComponent v-model.capitalize="text" />
```
```vue
<script setup lang="ts">
const [model, modifiers] = defineModel<string>()
console.log(modifiers) // { capitalize: true }
</script>
```
### Transform with Modifiers
Use `get` and `set` options:
```vue
<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
<template>
<input v-model="model" />
</template>
```
### Modifiers with Named v-model
```vue-html
<MyComponent v-model:title.capitalize="title" />
```
```ts
const [title, titleModifiers] = defineModel<string>('title')
console.log(titleModifiers) // { capitalize: true }
```
## Manual Implementation (Pre-3.4)
Understanding what `defineModel` does under the hood:
```vue
<script setup lang="ts">
const props = defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
```
For named v-model `v-model:title`:
```ts
defineProps<{ title: string }>()
defineEmits<{ 'update:title': [value: string] }>()
```
## Typing defineModel
```ts
// Basic
const model = defineModel<string>()
// ^? Ref<string | undefined>
// Required (removes undefined)
const model = defineModel<string>({ required: true })
// ^? Ref<string>
// With modifiers
const [model, modifiers] = defineModel<string, 'trim' | 'capitalize'>()
// ^? Record<'trim' | 'capitalize', true | undefined>
```
## Warning: Default Values
Having a default in `defineModel` without providing a value from parent causes desync:
```vue
<!-- Child: model is 1 -->
<script setup>
const model = defineModel({ default: 1 })
</script>
<!-- Parent: myRef is undefined -->
<script setup>
const myRef = ref()
</script>
<Child v-model="myRef" />
```
<!--
Source references:
- https://vuejs.org/guide/components/v-model.html
- https://vuejs.org/api/sfc-script-setup.html#definemodel
-->

View File

@@ -0,0 +1,289 @@
---
name: vue-reactivity-system
description: Core reactivity primitives - ref, reactive, computed, and watchers
---
# Vue Reactivity System
Vue's reactivity system enables automatic tracking of state changes and DOM updates.
## ref()
Create reactive primitive values with `ref()`. Access/modify via `.value` in JavaScript, auto-unwrapped in templates.
```ts
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
```
```vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
```
### Typing refs
```ts
import { ref } from 'vue'
import type { Ref } from 'vue'
// Type inference
const year = ref(2020) // Ref<number>
// Explicit generic
const name = ref<string | null>(null)
// Ref type annotation
const id: Ref<string | number> = ref('abc')
```
## reactive()
Create reactive objects. No `.value` needed, but cannot reassign the entire object.
```ts
import { reactive } from 'vue'
interface State {
count: number
name: string
}
const state: State = reactive({
count: 0,
name: 'Vue'
})
state.count++ // reactive
```
### Limitations of reactive()
1. **Only works with objects** - not primitives
2. **Cannot replace entire object** - loses reactivity
3. **Destructuring loses reactivity** - use `toRefs()` instead
```ts
const state = reactive({ count: 0 })
// ❌ Loses reactivity
let { count } = state
// ✅ Use toRefs
import { toRefs } from 'vue'
const { count } = toRefs(state)
```
## Recommendation
Use `ref()` as the primary API for declaring reactive state - it works with any value type and has consistent behavior.
## Deep Reactivity
Both `ref()` and `reactive()` are deeply reactive by default:
```ts
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
// These trigger updates
obj.value.nested.count++
obj.value.arr.push('baz')
```
Use `shallowRef()` or `shallowReactive()` to opt out of deep reactivity for performance.
## DOM Update Timing
DOM updates are batched and asynchronous. Use `nextTick()` to wait for updates:
```ts
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
count.value++
await nextTick()
// DOM is now updated
}
```
## Ref Unwrapping Rules
- **In templates**: Top-level refs auto-unwrap
- **In reactive objects**: Refs auto-unwrap when accessed as properties
- **In arrays/collections**: Refs do NOT auto-unwrap
```ts
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 0 (unwrapped)
const books = reactive([ref('Vue Guide')])
console.log(books[0].value) // Need .value
```
## computed()
Derive values from reactive state with automatic caching. Only re-evaluates when dependencies change.
```ts
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// Readonly computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Writable computed
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue: string) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
```
### Computed Best Practices
- **Getters should be pure** - no side effects, no mutating other state
- **Don't mutate computed values** - mutate the source instead
- **Use computed over methods** for derived data (caching benefit)
```ts
// ✅ Cached - only recalculates when items changes
const activeItems = computed(() => items.value.filter(x => x.active))
// ❌ Not cached - runs on every render
function getActiveItems() {
return items.value.filter(x => x.active)
}
```
## watch()
Explicitly watch reactive sources and run side effects when they change. Lazy by default.
```ts
import { ref, watch } from 'vue'
const id = ref(1)
watch(id, async (newId, oldId) => {
const data = await fetchData(newId)
// handle data...
})
```
### Watch Source Types
```ts
const x = ref(0)
const obj = reactive({ count: 0 })
// Single ref
watch(x, (newX) => console.log(newX))
// Getter function
watch(() => obj.count, (count) => console.log(count))
// Multiple sources
watch([x, () => obj.count], ([newX, newCount]) => {
console.log(newX, newCount)
})
```
### Watch Options
```ts
watch(source, callback, {
immediate: true, // Run immediately on creation
deep: true, // Watch nested properties
once: true, // Trigger only once (3.4+)
flush: 'post' // Run after DOM update
})
```
## watchEffect()
Automatically tracks dependencies and runs immediately. Re-runs when any tracked dependency changes.
```ts
import { ref, watchEffect } from 'vue'
const todoId = ref(1)
const data = ref(null)
watchEffect(async () => {
const response = await fetch(`/api/todos/${todoId.value}`)
data.value = await response.json()
})
```
### watch vs watchEffect
| Feature | watch | watchEffect |
|---------|-------|-------------|
| Dependency tracking | Explicit | Automatic |
| Lazy | Yes | No (immediate) |
| Access old value | Yes | No |
| Best for | Specific sources | Multiple dependencies |
## Watcher Cleanup (3.5+)
Cancel stale async operations:
```ts
import { watch, onWatcherCleanup } from 'vue'
watch(id, async (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal })
onWatcherCleanup(() => controller.abort())
})
```
## Stopping Watchers
```ts
const stop = watch(source, callback)
const stop2 = watchEffect(() => { /* ... */ })
// Stop manually
stop()
stop2()
// Pause/Resume (3.5+)
const { stop, pause, resume } = watchEffect(() => { /* ... */ })
```
<!--
Source references:
- https://vuejs.org/guide/essentials/reactivity-fundamentals.html
- https://vuejs.org/guide/essentials/computed.html
- https://vuejs.org/guide/essentials/watchers.html
- https://vuejs.org/api/reactivity-core.html
-->

View File

@@ -0,0 +1,262 @@
---
name: composables
description: Encapsulate and reuse stateful logic with the Composition API
---
# Composables
Composables are functions that encapsulate and reuse stateful logic using Composition API.
## What is a Composable?
A composable is a function that:
- Uses Composition API functions (ref, computed, onMounted, etc.)
- Manages reactive state
- Returns reactive state and/or functions
- Name starts with "use" by convention
## Basic Example
```ts
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
```
Usage:
```vue
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
Mouse: {{ x }}, {{ y }}
</template>
```
## Async Composable
Handle async data fetching:
```ts
// composables/useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
watchEffect(async () => {
data.value = null
error.value = null
isLoading.value = true
try {
const response = await fetch(toValue(url))
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
})
return { data, error, isLoading }
}
```
Usage with reactive URL:
```vue
<script setup lang="ts">
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
const { data, error, isLoading } = useFetch(() => `/api/users/${userId.value}`)
// Changing userId triggers refetch
function nextUser() {
userId.value++
}
</script>
```
## Composable Conventions
### Naming
- Always start with `use` (useMouse, useFetch, useCounter)
- Use camelCase
### Input Arguments
Accept refs or getters for reactivity:
```ts
import { toValue, type MaybeRefOrGetter } from 'vue'
function useFeature(input: MaybeRefOrGetter<string>) {
// toValue handles ref, getter, or plain value
const value = toValue(input)
// For reactive tracking, call toValue inside watchEffect/computed
watchEffect(() => {
console.log(toValue(input))
})
}
```
### Return Values
Return a plain object with refs (not reactive):
```ts
// ✅ Good - refs can be destructured
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
// Usage - maintains reactivity
const { count, increment } = useCounter()
```
```ts
// ❌ Avoid - reactive loses connection on destructure
export function useCounter() {
const state = reactive({ count: 0 })
return state
}
// Loses reactivity
const { count } = useCounter()
```
## Composing Composables
Composables can use other composables:
```ts
// composables/useEventListener.ts
import { onMounted, onUnmounted, type MaybeRefOrGetter, toValue } from 'vue'
export function useEventListener(
target: MaybeRefOrGetter<EventTarget>,
event: string,
callback: EventListener
) {
onMounted(() => toValue(target).addEventListener(event, callback))
onUnmounted(() => toValue(target).removeEventListener(event, callback))
}
```
```ts
// composables/useMouse.ts - using useEventListener
import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = (event as MouseEvent).pageX
y.value = (event as MouseEvent).pageY
})
return { x, y }
}
```
## Side Effects
### SSR Considerations
Run DOM-specific code only in browser:
```ts
export function useWindowSize() {
const width = ref(0)
const height = ref(0)
onMounted(() => {
// Only runs in browser
width.value = window.innerWidth
height.value = window.innerHeight
})
return { width, height }
}
```
### Cleanup
Always clean up side effects:
```ts
export function useInterval(callback: () => void, delay: number) {
const id = ref<number | undefined>()
onMounted(() => {
id.value = setInterval(callback, delay)
})
onUnmounted(() => {
if (id.value) clearInterval(id.value)
})
}
```
## Usage Restrictions
Composables must be called:
- Synchronously in `<script setup>` or `setup()`
- Can be called in lifecycle hooks like `onMounted()`
```ts
// ✅ Works
<script setup>
const { x, y } = useMouse()
</script>
// ❌ Won't work
setTimeout(() => {
const { x, y } = useMouse() // No active component
}, 100)
```
Exception: `<script setup>` allows composables after `await`.
## vs Other Patterns
**vs Mixins**: Composables are explicit (no naming collisions, clear source)
**vs Renderless Components**: Composables have no component overhead
**vs React Hooks**: Similar concept, but Vue's reactivity is different - no rules about hook order or dependency arrays
<!--
Source references:
- https://vuejs.org/guide/reusability/composables.html
-->

View File

@@ -0,0 +1,224 @@
---
name: custom-directives
description: Create reusable directives for low-level DOM manipulation
---
# Custom Directives
Custom directives provide low-level DOM access for reusable behavior.
## When to Use
Use custom directives when:
- You need direct DOM manipulation
- The behavior can't be achieved with components or composables
- You need to apply behavior to native elements
## Basic Example
```vue
<script setup lang="ts">
// v-focus directive
const vFocus = {
mounted: (el: HTMLElement) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
```
## Directive Hooks
```ts
const myDirective = {
// Before element attributes/listeners are applied
created(el, binding, vnode) {},
// Before element is inserted into DOM
beforeMount(el, binding, vnode) {},
// After element and children are mounted
mounted(el, binding, vnode) {},
// Before parent component updates
beforeUpdate(el, binding, vnode, prevVnode) {},
// After parent component updates
updated(el, binding, vnode, prevVnode) {},
// Before parent component unmounts
beforeUnmount(el, binding, vnode) {},
// After parent component unmounts
unmounted(el, binding, vnode) {}
}
```
## Hook Arguments
```ts
interface DirectiveBinding<T = any> {
value: T // v-my-dir="value"
oldValue: T // Previous value (beforeUpdate/updated only)
arg?: string // v-my-dir:arg
modifiers: Record<string, boolean> // v-my-dir.foo.bar → { foo: true, bar: true }
instance: ComponentPublicInstance // Component using the directive
dir: ObjectDirective // Directive definition object
}
```
Example usage:
```vue-html
<div v-example:foo.bar="baz">
```
```ts
// binding object:
{
arg: 'foo',
modifiers: { bar: true },
value: /* value of baz */,
oldValue: /* previous value */
}
```
## Function Shorthand
When you only need `mounted` and `updated` with same behavior:
```ts
// Full form
const vColor = {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
}
// Shorthand (same behavior)
const vColor = (el: HTMLElement, binding: DirectiveBinding<string>) => {
el.style.color = binding.value
}
```
## Global Registration
```ts
// main.ts
const app = createApp(App)
app.directive('focus', {
mounted: (el) => el.focus()
})
// Shorthand
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
```
## Object Literals
Pass multiple values:
```vue-html
<div v-demo="{ color: 'white', text: 'hello' }">
```
```ts
const vDemo = (el: HTMLElement, binding: DirectiveBinding<{ color: string; text: string }>) => {
console.log(binding.value.color) // 'white'
console.log(binding.value.text) // 'hello'
}
```
## Dynamic Arguments
```vue-html
<div v-my-directive:[dynamicArg]="value">
```
## Practical Examples
### v-click-outside
```ts
const vClickOutside = {
mounted(el: HTMLElement, binding: DirectiveBinding<() => void>) {
el._clickOutside = (event: MouseEvent) => {
if (!el.contains(event.target as Node)) {
binding.value()
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el: HTMLElement) {
document.removeEventListener('click', el._clickOutside)
}
}
```
### v-tooltip
```ts
const vTooltip = {
mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
el.setAttribute('title', binding.value)
},
updated(el: HTMLElement, binding: DirectiveBinding<string>) {
el.setAttribute('title', binding.value)
}
}
```
### v-permission
```ts
const vPermission = {
mounted(el: HTMLElement, binding: DirectiveBinding<string>) {
if (!hasPermission(binding.value)) {
el.parentNode?.removeChild(el)
}
}
}
```
## TypeScript: Global Directives
```ts
// directives/highlight.ts
import type { Directive } from 'vue'
export type HighlightDirective = Directive<HTMLElement, string>
declare module 'vue' {
export interface ComponentCustomProperties {
vHighlight: HighlightDirective
}
}
export default {
mounted: (el, binding) => {
el.style.backgroundColor = binding.value
}
} satisfies HighlightDirective
```
## Usage on Components
⚠️ **Not recommended** - directives apply to root element, which can be unpredictable with multi-root components.
```vue-html
<!-- Applies to MyComponent's root element -->
<MyComponent v-my-directive />
```
<!--
Source references:
- https://vuejs.org/guide/reusability/custom-directives.html
-->

View File

@@ -0,0 +1,247 @@
---
name: script-setup
description: Compile-time syntactic sugar for Composition API in SFCs
---
# Script Setup
`<script setup>` is the recommended syntax for Composition API in Single-File Components.
## Basic Syntax
```vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
```
## Benefits
- Less boilerplate (no `export default`, no `return`)
- TypeScript type inference
- Better runtime performance
- Better IDE support
## Top-Level Bindings
All top-level bindings are automatically available in template:
```vue
<script setup lang="ts">
// Variables
const msg = 'Hello'
// Imports
import { capitalize } from './helpers'
import MyComponent from './MyComponent.vue'
// Functions
function greet() {
console.log(msg)
}
</script>
<template>
<MyComponent />
<p>{{ capitalize(msg) }}</p>
<button @click="greet">Greet</button>
</template>
```
## Compiler Macros
These are available without import:
- `defineProps()` - declare props
- `defineEmits()` - declare emits
- `defineModel()` - declare v-model (3.4+)
- `defineExpose()` - expose public instance properties
- `defineOptions()` - declare component options (3.3+)
- `defineSlots()` - type slot props (3.3+)
- `withDefaults()` - provide prop defaults
## Using Components
Import and use directly without registration:
```vue
<script setup lang="ts">
import MyComponent from './MyComponent.vue'
import { MyButton } from './components'
</script>
<template>
<MyComponent />
<MyButton />
</template>
```
### Dynamic Components
```vue
<script setup lang="ts">
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="condition ? Foo : Bar" />
</template>
```
### Recursive Components
SFCs can reference themselves by filename:
```vue
<!-- FooBar.vue can use <FooBar/> in its template -->
```
## Custom Directives
Variables starting with `v` are available as directives:
```vue
<script setup lang="ts">
const vFocus = {
mounted: (el: HTMLElement) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
```
## defineExpose()
Components are closed by default. Use `defineExpose` to expose properties:
```vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const publicMethod = () => console.log('called')
defineExpose({
count,
publicMethod
})
</script>
```
Parent can access via template ref:
```ts
const childRef = ref()
childRef.value.count // accessible
childRef.value.publicMethod() // accessible
```
## defineOptions() (3.3+)
Declare component options without separate `<script>` block:
```vue
<script setup lang="ts">
defineOptions({
inheritAttrs: false,
name: 'CustomName'
})
</script>
```
## defineSlots() (3.3+)
Type slot props for IDE support:
```vue
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): any
header(props: { title: string }): any
}>()
</script>
```
## useSlots() & useAttrs()
Access slots and attrs in script:
```vue
<script setup lang="ts">
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
```
## Top-Level await
Async setup with Suspense:
```vue
<script setup lang="ts">
const data = await fetch('/api/data').then(r => r.json())
</script>
```
> Note: Requires `<Suspense>` boundary in parent.
## Generic Components (TypeScript)
```vue
<script setup lang="ts" generic="T extends string | number">
defineProps<{
items: T[]
selected: T
}>()
</script>
```
Multiple generics with constraints:
```vue
<script setup lang="ts" generic="T extends Item, U extends string">
import type { Item } from './types'
defineProps<{
item: T
label: U
}>()
</script>
```
## Alongside Normal Script
For advanced cases like named exports or one-time side effects:
```vue
<script lang="ts">
// Runs once when module is imported
runSideEffect()
export const exportedValue = 'hello'
</script>
<script setup lang="ts">
// Runs for each component instance
</script>
```
<!--
Source references:
- https://vuejs.org/api/sfc-script-setup.html
-->

View File

@@ -0,0 +1,212 @@
---
name: template-refs
description: Access DOM elements and child component instances directly
---
# Template Refs
Template refs provide direct access to DOM elements and child component instances.
## Basic Usage (3.5+)
Use `useTemplateRef()` for type-safe refs:
```vue
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="input" />
</template>
```
## Legacy Approach (Pre-3.5)
Use a ref with matching name:
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>
```
## Refs in v-for
Collect multiple refs into an array:
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const items = ref([1, 2, 3])
const itemRefs = ref<HTMLLIElement[]>([])
</script>
<template>
<ul>
<li v-for="item in items" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
```
> Note: ref array order is not guaranteed to match source array order.
## Function Refs
Use a function for full control:
```vue
<script setup lang="ts">
const elements = new Map<number, HTMLElement>()
function setItemRef(el: HTMLElement | null, id: number) {
if (el) {
elements.set(id, el)
} else {
elements.delete(id)
}
}
</script>
<template>
<div v-for="item in items" :ref="(el) => setItemRef(el, item.id)">
{{ item.name }}
</div>
</template>
```
## Component Refs
Access child component instance:
```vue
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
import Child from './Child.vue'
type ChildInstance = InstanceType<typeof Child>
const childRef = useTemplateRef<ChildInstance>('child')
onMounted(() => {
// Access exposed properties/methods
childRef.value?.someMethod()
})
</script>
<template>
<Child ref="child" />
</template>
```
### Exposing Component Properties
By default, `<script setup>` components are closed. Use `defineExpose`:
```vue
<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
// Only these are accessible via ref
defineExpose({
count,
increment
})
</script>
```
## Common Use Cases
### Focus Management
```vue
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('input')
function focusInput() {
inputRef.value?.focus()
}
</script>
```
### Scroll Control
```vue
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const containerRef = useTemplateRef<HTMLDivElement>('container')
function scrollToBottom() {
const el = containerRef.value
if (el) {
el.scrollTop = el.scrollHeight
}
}
</script>
```
### Third-Party Library Integration
```vue
<script setup lang="ts">
import { useTemplateRef, onMounted, onUnmounted } from 'vue'
import Chart from 'chart.js'
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvas')
let chart: Chart | null = null
onMounted(() => {
if (canvasRef.value) {
chart = new Chart(canvasRef.value, {
// options
})
}
})
onUnmounted(() => {
chart?.destroy()
})
</script>
<template>
<canvas ref="canvas"></canvas>
</template>
```
## Important Notes
1. **Refs are null before mount** - access in `onMounted` or later
2. **Conditional refs can be null** - elements with `v-if` may not exist
3. **Use optional chaining** - `ref.value?.method()` for safety
4. **Avoid overusing** - prefer declarative approaches when possible
<!--
Source references:
- https://vuejs.org/guide/essentials/template-refs.html
- https://vuejs.org/guide/typescript/composition-api.html#typing-template-refs
-->

View File

@@ -0,0 +1,274 @@
---
name: typescript-integration
description: Type Vue components with TypeScript in Composition API
---
# TypeScript with Vue
Vue provides excellent TypeScript support in Composition API.
## Typing Props
### Type-based declaration (recommended)
```vue
<script setup lang="ts">
interface Props {
title: string
count?: number
items: string[]
user: { name: string; age: number }
}
const props = defineProps<Props>()
```
### With defaults (3.5+)
```vue
<script setup lang="ts">
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one'] } = defineProps<Props>()
```
### With withDefaults (3.4 and below)
```vue
<script setup lang="ts">
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
```
### Complex prop types
```vue
<script setup lang="ts">
interface Book {
title: string
author: string
year: number
}
const props = defineProps<{
book: Book
callback: (id: number) => void
}>()
```
## Typing Emits
```vue
<script setup lang="ts">
// Call signature syntax
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// Named tuple syntax (3.3+, more concise)
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
</script>
```
## Typing ref()
```ts
import { ref } from 'vue'
import type { Ref } from 'vue'
// Inferred
const count = ref(0) // Ref<number>
// Explicit generic
const name = ref<string>() // Ref<string | undefined>
// Type annotation
const id: Ref<string | number> = ref('abc')
```
## Typing reactive()
```ts
import { reactive } from 'vue'
interface State {
count: number
name: string
}
// Use interface on variable
const state: State = reactive({
count: 0,
name: 'Vue'
})
```
## Typing computed()
```ts
import { computed } from 'vue'
// Inferred from getter
const double = computed(() => count.value * 2) // ComputedRef<number>
// Explicit generic
const result = computed<string>(() => {
return String(value.value)
})
```
## Typing Event Handlers
```vue
<script setup lang="ts">
function handleChange(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}
</script>
<template>
<input @change="handleChange" />
</template>
```
## Typing Template Refs
### useTemplateRef (3.5+)
```vue
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="input" />
</template>
```
### Legacy ref approach
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})
</script>
<template>
<input ref="inputRef" />
</template>
```
## Typing Component Refs
```vue
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import MyComponent from './MyComponent.vue'
type MyComponentInstance = InstanceType<typeof MyComponent>
const compRef = useTemplateRef<MyComponentInstance>('comp')
</script>
<template>
<MyComponent ref="comp" />
</template>
```
## Typing Provide/Inject
```ts
import { provide, inject } from 'vue'
import type { InjectionKey, Ref } from 'vue'
// Define typed key
const countKey = Symbol() as InjectionKey<Ref<number>>
// Provider
provide(countKey, ref(0))
// Injector
const count = inject(countKey) // Ref<number> | undefined
// With default
const count = inject(countKey, ref(0)) // Ref<number>
// String key with explicit type
const foo = inject<string>('foo')
```
## Typing defineModel (3.4+)
```ts
// Basic
const model = defineModel<string>()
// ^? Ref<string | undefined>
// Required
const model = defineModel<string>({ required: true })
// ^? Ref<string>
// With modifiers
const [model, modifiers] = defineModel<string, 'trim' | 'uppercase'>()
// ^? Record<'trim' | 'uppercase', true | undefined>
```
## Global Custom Properties
Extend `ComponentCustomProperties` for global properties:
```ts
// types/vue.d.ts
declare module 'vue' {
interface ComponentCustomProperties {
$http: typeof axios
$translate: (key: string) => string
}
}
```
## Generic Components
```vue
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
selected: T
}>()
defineEmits<{
select: [item: T]
}>()
</script>
```
<!--
Source references:
- https://vuejs.org/guide/typescript/composition-api.html
- https://vuejs.org/guide/typescript/overview.html
-->