first
This commit is contained in:
5
.agents/skills/vue/GENERATION.md
Normal file
5
.agents/skills/vue/GENERATION.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Generation Info
|
||||
|
||||
- **Source:** `sources/vue`
|
||||
- **Git SHA:** `dcf363038ab2f5b4571028669cf893d084b6b207`
|
||||
- **Generated:** 2026-01-28
|
||||
63
.agents/skills/vue/SKILL.md
Normal file
63
.agents/skills/vue/SKILL.md
Normal 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
|
||||
246
.agents/skills/vue/references/advanced-async-suspense.md
Normal file
246
.agents/skills/vue/references/advanced-async-suspense.md
Normal 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
|
||||
-->
|
||||
174
.agents/skills/vue/references/advanced-provide-inject.md
Normal file
174
.agents/skills/vue/references/advanced-provide-inject.md
Normal 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
|
||||
-->
|
||||
139
.agents/skills/vue/references/components-emits.md
Normal file
139
.agents/skills/vue/references/components-emits.md
Normal 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
|
||||
-->
|
||||
192
.agents/skills/vue/references/components-lifecycle.md
Normal file
192
.agents/skills/vue/references/components-lifecycle.md
Normal 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
|
||||
-->
|
||||
211
.agents/skills/vue/references/components-props.md
Normal file
211
.agents/skills/vue/references/components-props.md
Normal 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
|
||||
-->
|
||||
201
.agents/skills/vue/references/components-slots.md
Normal file
201
.agents/skills/vue/references/components-slots.md
Normal 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
|
||||
-->
|
||||
183
.agents/skills/vue/references/components-v-model.md
Normal file
183
.agents/skills/vue/references/components-v-model.md
Normal 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
|
||||
-->
|
||||
289
.agents/skills/vue/references/core-reactivity.md
Normal file
289
.agents/skills/vue/references/core-reactivity.md
Normal 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
|
||||
-->
|
||||
262
.agents/skills/vue/references/features-composables.md
Normal file
262
.agents/skills/vue/references/features-composables.md
Normal 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
|
||||
-->
|
||||
224
.agents/skills/vue/references/features-directives.md
Normal file
224
.agents/skills/vue/references/features-directives.md
Normal 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
|
||||
-->
|
||||
247
.agents/skills/vue/references/features-script-setup.md
Normal file
247
.agents/skills/vue/references/features-script-setup.md
Normal 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
|
||||
-->
|
||||
212
.agents/skills/vue/references/features-template-refs.md
Normal file
212
.agents/skills/vue/references/features-template-refs.md
Normal 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
|
||||
-->
|
||||
274
.agents/skills/vue/references/features-typescript.md
Normal file
274
.agents/skills/vue/references/features-typescript.md
Normal 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
|
||||
-->
|
||||
Reference in New Issue
Block a user