@dianzhong/create-harness-app 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/harness/full/.claude/agents/code-reviewer.md +1 -1
- package/templates/harness/full/.claude/agents/harness-reviewer.md +0 -2
- package/templates/harness/full/.claude/rules/skills-mcp.md +2 -3
- package/templates/harness/full/.claude/settings.json +1 -1
- package/templates/harness/full/docs/ai-harness.md +4 -6
- package/templates/harness/full/docs/harness-quick-reference.md +1 -1
- package/templates/harness/full/docs/review-checklist.md +1 -1
- package/templates/harness/full/scripts/verify-skills.mjs +6 -61
- package/templates/harness/full/scripts/verify-skills.test.mjs +1 -11
- package/templates/harness/full/.agents/skills/find-skills/SKILL.md +0 -143
- package/templates/harness/full/.agents/skills/vue-best-practices/LICENSE.md +0 -21
- package/templates/harness/full/.agents/skills/vue-best-practices/SKILL.md +0 -155
- package/templates/harness/full/.agents/skills/vue-best-practices/SYNC.md +0 -5
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-class-based-technique.md +0 -258
- package/templates/harness/full/.agents/skills/vue-best-practices/references/animation-state-driven-technique.md +0 -287
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-async.md +0 -99
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-data-flow.md +0 -313
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-fallthrough-attrs.md +0 -179
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-keep-alive.md +0 -139
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-slots.md +0 -226
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-suspense.md +0 -231
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-teleport.md +0 -110
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition-group.md +0 -131
- package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md +0 -135
- package/templates/harness/full/.agents/skills/vue-best-practices/references/composables.md +0 -303
- package/templates/harness/full/.agents/skills/vue-best-practices/references/directives.md +0 -168
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +0 -177
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +0 -185
- package/templates/harness/full/.agents/skills/vue-best-practices/references/perf-virtualize-large-lists.md +0 -182
- package/templates/harness/full/.agents/skills/vue-best-practices/references/plugins.md +0 -178
- package/templates/harness/full/.agents/skills/vue-best-practices/references/reactivity.md +0 -371
- package/templates/harness/full/.agents/skills/vue-best-practices/references/render-functions.md +0 -227
- package/templates/harness/full/.agents/skills/vue-best-practices/references/sfc.md +0 -355
- package/templates/harness/full/.agents/skills/vue-best-practices/references/state-management.md +0 -138
- package/templates/harness/full/.agents/skills/vue-best-practices/references/updated-hook-performance.md +0 -193
- package/templates/harness/full/AGENTS.md +0 -3
- package/templates/harness/full/GEMINI.md +0 -3
package/templates/harness/full/.agents/skills/vue-best-practices/references/component-transition.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Transition Component Best Practices
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: Transition animates a single element or component; incorrect structure or keys prevent animations
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, transition, animation, performance, keys]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Transition Component Best Practices
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - `<Transition>` animates entering/leaving of a single element or component. It is ideal for toggling UI states, swapping views, or animating one component at a time.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Wrap a single element or component inside `<Transition>`
|
|
16
|
-
- Provide a `key` when switching between same element types
|
|
17
|
-
- Use `mode="out-in"` when you need sequential swaps
|
|
18
|
-
- Prefer `transform` and `opacity` for smooth animations
|
|
19
|
-
|
|
20
|
-
## Use Transition for a Single Root Element
|
|
21
|
-
|
|
22
|
-
`<Transition>` only supports one direct child. Wrap multiple nodes in a single element or component.
|
|
23
|
-
|
|
24
|
-
**BAD:**
|
|
25
|
-
|
|
26
|
-
```vue
|
|
27
|
-
<template>
|
|
28
|
-
<Transition name="fade">
|
|
29
|
-
<h3>Title</h3>
|
|
30
|
-
<p>Description</p>
|
|
31
|
-
</Transition>
|
|
32
|
-
</template>
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
**GOOD:**
|
|
36
|
-
|
|
37
|
-
```vue
|
|
38
|
-
<template>
|
|
39
|
-
<Transition name="fade">
|
|
40
|
-
<div>
|
|
41
|
-
<h3>Title</h3>
|
|
42
|
-
<p>Description</p>
|
|
43
|
-
</div>
|
|
44
|
-
</Transition>
|
|
45
|
-
</template>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Force Transitions Between Same Element Types
|
|
49
|
-
|
|
50
|
-
Vue reuses the same DOM element when the tag type does not change. Add `key` so Vue treats it as a new element and triggers enter/leave.
|
|
51
|
-
|
|
52
|
-
**BAD:**
|
|
53
|
-
|
|
54
|
-
```vue
|
|
55
|
-
<template>
|
|
56
|
-
<Transition name="fade">
|
|
57
|
-
<p v-if="isActive">Active</p>
|
|
58
|
-
<p v-else>Inactive</p>
|
|
59
|
-
</Transition>
|
|
60
|
-
</template>
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**GOOD:**
|
|
64
|
-
|
|
65
|
-
```vue
|
|
66
|
-
<template>
|
|
67
|
-
<Transition name="fade" mode="out-in">
|
|
68
|
-
<p v-if="isActive" key="active">Active</p>
|
|
69
|
-
<p v-else key="inactive">Inactive</p>
|
|
70
|
-
</Transition>
|
|
71
|
-
</template>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Use `mode` to Avoid Overlap During Swaps
|
|
75
|
-
|
|
76
|
-
When swapping components or views, use `mode="out-in"` to prevent both from being visible at the same time.
|
|
77
|
-
|
|
78
|
-
**BAD:**
|
|
79
|
-
|
|
80
|
-
```vue
|
|
81
|
-
<template>
|
|
82
|
-
<Transition name="fade">
|
|
83
|
-
<component :is="currentView" />
|
|
84
|
-
</Transition>
|
|
85
|
-
</template>
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**GOOD:**
|
|
89
|
-
|
|
90
|
-
```vue
|
|
91
|
-
<template>
|
|
92
|
-
<Transition name="fade" mode="out-in">
|
|
93
|
-
<component :is="currentView" :key="currentView" />
|
|
94
|
-
</Transition>
|
|
95
|
-
</template>
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
## Animate `transform` and `opacity` for Performance
|
|
99
|
-
|
|
100
|
-
Avoid layout-triggering properties such as `height`, `margin`, or `top`. Use `transform` and `opacity` for smooth, GPU-friendly transitions.
|
|
101
|
-
|
|
102
|
-
**BAD:**
|
|
103
|
-
|
|
104
|
-
```css
|
|
105
|
-
.slide-enter-active,
|
|
106
|
-
.slide-leave-active {
|
|
107
|
-
transition: height 0.3s ease;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.slide-enter-from,
|
|
111
|
-
.slide-leave-to {
|
|
112
|
-
height: 0;
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
**GOOD:**
|
|
117
|
-
|
|
118
|
-
```css
|
|
119
|
-
.slide-enter-active,
|
|
120
|
-
.slide-leave-active {
|
|
121
|
-
transition:
|
|
122
|
-
transform 0.3s ease,
|
|
123
|
-
opacity 0.3s ease;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.slide-enter-from {
|
|
127
|
-
transform: translateX(-12px);
|
|
128
|
-
opacity: 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.slide-leave-to {
|
|
132
|
-
transform: translateX(12px);
|
|
133
|
-
opacity: 0;
|
|
134
|
-
}
|
|
135
|
-
```
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Composable Organization Patterns
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: Well-structured composables improve maintainability, reusability, and update performance
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, composables, composition-api, code-organization, api-design, readonly, utilities]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Composable Organization Patterns
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - Treat composables as reusable, stateful building blocks and keep their code organized by feature concern. This keeps large components maintainable and prevents hard-to-debug mutation and API design issues.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Compose complex behavior from small, focused composables
|
|
16
|
-
- Use options objects for composables with multiple optional parameters
|
|
17
|
-
- Return readonly state when updates must flow through explicit actions
|
|
18
|
-
- Keep pure utility functions as plain utilities, not composables
|
|
19
|
-
- Organize composable and component code by feature concern, and extract composables when components grow
|
|
20
|
-
|
|
21
|
-
## Compose Composables from Smaller Primitives
|
|
22
|
-
|
|
23
|
-
**BAD:**
|
|
24
|
-
|
|
25
|
-
```vue
|
|
26
|
-
<script setup>
|
|
27
|
-
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
28
|
-
|
|
29
|
-
const x = ref(0)
|
|
30
|
-
const y = ref(0)
|
|
31
|
-
const inside = ref(false)
|
|
32
|
-
const el = ref(null)
|
|
33
|
-
|
|
34
|
-
function onMove(e) {
|
|
35
|
-
x.value = e.pageX
|
|
36
|
-
y.value = e.pageY
|
|
37
|
-
if (!el.value) return
|
|
38
|
-
const r = el.value.getBoundingClientRect()
|
|
39
|
-
inside.value = x.value >= r.left && x.value <= r.right && y.value >= r.top && y.value <= r.bottom
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
onMounted(() => window.addEventListener('mousemove', onMove))
|
|
43
|
-
onUnmounted(() => window.removeEventListener('mousemove', onMove))
|
|
44
|
-
</script>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**GOOD:**
|
|
48
|
-
|
|
49
|
-
```javascript
|
|
50
|
-
// composables/useEventListener.js
|
|
51
|
-
import { onMounted, onUnmounted, toValue } from 'vue'
|
|
52
|
-
|
|
53
|
-
export function useEventListener(target, event, callback) {
|
|
54
|
-
onMounted(() => toValue(target).addEventListener(event, callback))
|
|
55
|
-
onUnmounted(() => toValue(target).removeEventListener(event, callback))
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
```javascript
|
|
60
|
-
// composables/useMouse.js
|
|
61
|
-
import { ref } from 'vue'
|
|
62
|
-
|
|
63
|
-
import { useEventListener } from './useEventListener'
|
|
64
|
-
|
|
65
|
-
export function useMouse() {
|
|
66
|
-
const x = ref(0)
|
|
67
|
-
const y = ref(0)
|
|
68
|
-
|
|
69
|
-
useEventListener(window, 'mousemove', (e) => {
|
|
70
|
-
x.value = e.pageX
|
|
71
|
-
y.value = e.pageY
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
return { x, y }
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
```javascript
|
|
79
|
-
// composables/useMouseInElement.js
|
|
80
|
-
import { computed } from 'vue'
|
|
81
|
-
|
|
82
|
-
import { useMouse } from './useMouse'
|
|
83
|
-
|
|
84
|
-
export function useMouseInElement(elementRef) {
|
|
85
|
-
const { x, y } = useMouse()
|
|
86
|
-
|
|
87
|
-
const isOutside = computed(() => {
|
|
88
|
-
if (!elementRef.value) return true
|
|
89
|
-
const rect = elementRef.value.getBoundingClientRect()
|
|
90
|
-
return (
|
|
91
|
-
x.value < rect.left || x.value > rect.right || y.value < rect.top || y.value > rect.bottom
|
|
92
|
-
)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
return { x, y, isOutside }
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Use Options Object Pattern for Composable Parameters
|
|
100
|
-
|
|
101
|
-
**BAD:**
|
|
102
|
-
|
|
103
|
-
```javascript
|
|
104
|
-
export function useFetch(url, method, headers, timeout, retries, immediate) {
|
|
105
|
-
// hard to read and easy to misorder
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
useFetch('/api/users', 'GET', null, 5000, 3, true)
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**GOOD:**
|
|
112
|
-
|
|
113
|
-
```javascript
|
|
114
|
-
export function useFetch(url, options = {}) {
|
|
115
|
-
const { method = 'GET', headers = {}, timeout = 30000, retries = 0, immediate = true } = options
|
|
116
|
-
|
|
117
|
-
// implementation
|
|
118
|
-
return { method, headers, timeout, retries, immediate }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
useFetch('/api/users', {
|
|
122
|
-
method: 'POST',
|
|
123
|
-
timeout: 5000,
|
|
124
|
-
retries: 3,
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
```typescript
|
|
129
|
-
interface UseCounterOptions {
|
|
130
|
-
initial?: number
|
|
131
|
-
min?: number
|
|
132
|
-
max?: number
|
|
133
|
-
step?: number
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function useCounter(options: UseCounterOptions = {}) {
|
|
137
|
-
const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options
|
|
138
|
-
// implementation
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
## Return Readonly State with Explicit Actions
|
|
143
|
-
|
|
144
|
-
**BAD:**
|
|
145
|
-
|
|
146
|
-
```javascript
|
|
147
|
-
export function useCart() {
|
|
148
|
-
const items = ref([])
|
|
149
|
-
const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))
|
|
150
|
-
return { items, total } // any consumer can mutate directly
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const { items } = useCart()
|
|
154
|
-
items.value.push({ id: 1, price: 10 })
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
**GOOD:**
|
|
158
|
-
|
|
159
|
-
```javascript
|
|
160
|
-
import { computed, readonly, ref } from 'vue'
|
|
161
|
-
|
|
162
|
-
export function useCart() {
|
|
163
|
-
const _items = ref([])
|
|
164
|
-
|
|
165
|
-
const total = computed(() =>
|
|
166
|
-
_items.value.reduce((sum, item) => sum + item.price * item.quantity, 0),
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
function addItem(product, quantity = 1) {
|
|
170
|
-
const existing = _items.value.find((item) => item.id === product.id)
|
|
171
|
-
if (existing) {
|
|
172
|
-
existing.quantity += quantity
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
_items.value.push({ ...product, quantity })
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function removeItem(productId) {
|
|
179
|
-
_items.value = _items.value.filter((item) => item.id !== productId)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
items: readonly(_items),
|
|
184
|
-
total,
|
|
185
|
-
addItem,
|
|
186
|
-
removeItem,
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## Keep Utilities as Utilities
|
|
192
|
-
|
|
193
|
-
**BAD:**
|
|
194
|
-
|
|
195
|
-
```javascript
|
|
196
|
-
export function useFormatters() {
|
|
197
|
-
const formatDate = (date) => new Intl.DateTimeFormat('en-US').format(date)
|
|
198
|
-
const formatCurrency = (amount) =>
|
|
199
|
-
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
|
|
200
|
-
return { formatDate, formatCurrency }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const { formatDate } = useFormatters()
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
**GOOD:**
|
|
207
|
-
|
|
208
|
-
```javascript
|
|
209
|
-
// utils/formatters.js
|
|
210
|
-
export function formatDate(date) {
|
|
211
|
-
return new Intl.DateTimeFormat('en-US').format(date)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function formatCurrency(amount) {
|
|
215
|
-
return new Intl.NumberFormat('en-US', {
|
|
216
|
-
style: 'currency',
|
|
217
|
-
currency: 'USD',
|
|
218
|
-
}).format(amount)
|
|
219
|
-
}
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
```javascript
|
|
223
|
-
// composables/useInvoiceSummary.js
|
|
224
|
-
import { computed } from 'vue'
|
|
225
|
-
|
|
226
|
-
import { formatCurrency } from '@/utils/formatters'
|
|
227
|
-
|
|
228
|
-
export function useInvoiceSummary(invoiceRef) {
|
|
229
|
-
const totalLabel = computed(() => formatCurrency(invoiceRef.value.total))
|
|
230
|
-
return { totalLabel }
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## Organize Composable and Component Code by Feature Concern
|
|
235
|
-
|
|
236
|
-
**BAD:**
|
|
237
|
-
|
|
238
|
-
```vue
|
|
239
|
-
<script setup>
|
|
240
|
-
import { computed, onMounted, ref, watch } from 'vue'
|
|
241
|
-
|
|
242
|
-
const searchQuery = ref('')
|
|
243
|
-
const items = ref([])
|
|
244
|
-
const selected = ref(null)
|
|
245
|
-
const showModal = ref(false)
|
|
246
|
-
const sortBy = ref('name')
|
|
247
|
-
const filter = ref('all')
|
|
248
|
-
const loading = ref(false)
|
|
249
|
-
|
|
250
|
-
const filtered = computed(() => items.value.filter((i) => i.category === filter.value))
|
|
251
|
-
function openModal() {
|
|
252
|
-
showModal.value = true
|
|
253
|
-
}
|
|
254
|
-
const sorted = computed(() => [...filtered.value].sort(/* ... */))
|
|
255
|
-
watch(searchQuery, () => {
|
|
256
|
-
/* ... */
|
|
257
|
-
})
|
|
258
|
-
onMounted(() => {
|
|
259
|
-
/* ... */
|
|
260
|
-
})
|
|
261
|
-
</script>
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
**GOOD:**
|
|
265
|
-
|
|
266
|
-
```vue
|
|
267
|
-
<script setup>
|
|
268
|
-
import { useItems } from '@/composables/useItems'
|
|
269
|
-
import { useSearch } from '@/composables/useSearch'
|
|
270
|
-
import { useSelectionModal } from '@/composables/useSelectionModal'
|
|
271
|
-
|
|
272
|
-
// Data
|
|
273
|
-
const { items, loading, fetchItems } = useItems()
|
|
274
|
-
|
|
275
|
-
// Search/filter/sort
|
|
276
|
-
const { query, visibleItems } = useSearch(items)
|
|
277
|
-
|
|
278
|
-
// Selection + modal
|
|
279
|
-
const { selectedItem, isModalOpen, selectItem, closeModal } = useSelectionModal()
|
|
280
|
-
</script>
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
```javascript
|
|
284
|
-
// composables/useItems.js
|
|
285
|
-
import { onMounted, ref } from 'vue'
|
|
286
|
-
|
|
287
|
-
export function useItems() {
|
|
288
|
-
const items = ref([])
|
|
289
|
-
const loading = ref(false)
|
|
290
|
-
|
|
291
|
-
async function fetchItems() {
|
|
292
|
-
loading.value = true
|
|
293
|
-
try {
|
|
294
|
-
items.value = await api.getItems()
|
|
295
|
-
} finally {
|
|
296
|
-
loading.value = false
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
onMounted(fetchItems)
|
|
301
|
-
return { items, loading, fetchItems }
|
|
302
|
-
}
|
|
303
|
-
```
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Directive Best Practices
|
|
3
|
-
impact: MEDIUM
|
|
4
|
-
impactDescription: Custom directives are powerful but easy to misuse; following patterns prevents leaks, invalid usage, and unclear abstractions
|
|
5
|
-
type: best-practice
|
|
6
|
-
tags: [vue3, directives, custom-directives, composition, typescript]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Directive Best Practices
|
|
10
|
-
|
|
11
|
-
**Impact: MEDIUM** - Directives are for low-level DOM access. Use them sparingly, keep them side-effect safe, and prefer components or composables when you need stateful or reusable UI behavior.
|
|
12
|
-
|
|
13
|
-
## Task List
|
|
14
|
-
|
|
15
|
-
- Use directives only when you need direct DOM access
|
|
16
|
-
- Do not mutate directive arguments or binding objects
|
|
17
|
-
- Clean up timers, listeners, and observers in `unmounted`
|
|
18
|
-
- Register directives in `<script setup>` with the `v-` prefix
|
|
19
|
-
- In TypeScript projects, type directive values and augment template directive types
|
|
20
|
-
- Prefer components or composables for complex behavior
|
|
21
|
-
|
|
22
|
-
## Treat Directive Arguments as Read-Only
|
|
23
|
-
|
|
24
|
-
Directive bindings are not reactive storage. Don’t write to them.
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
const vFocus = {
|
|
28
|
-
mounted(el, binding) {
|
|
29
|
-
// binding.value is read-only
|
|
30
|
-
el.focus()
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Avoid Directives on Components
|
|
36
|
-
|
|
37
|
-
Directives apply to DOM elements. When used on components, they attach to the root element and can break if the root changes.
|
|
38
|
-
|
|
39
|
-
**BAD:**
|
|
40
|
-
|
|
41
|
-
```vue
|
|
42
|
-
<MyInput v-focus />
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**GOOD:**
|
|
46
|
-
|
|
47
|
-
```vue
|
|
48
|
-
<!-- MyInput.vue -->
|
|
49
|
-
<script setup>
|
|
50
|
-
const vFocus = (el) => el.focus()
|
|
51
|
-
</script>
|
|
52
|
-
|
|
53
|
-
<template>
|
|
54
|
-
<input v-focus />
|
|
55
|
-
</template>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Clean Up Side Effects in `unmounted`
|
|
59
|
-
|
|
60
|
-
Any timers, listeners, or observers must be removed to avoid leaks.
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
const vResize = {
|
|
64
|
-
mounted(el) {
|
|
65
|
-
const observer = new ResizeObserver(() => {})
|
|
66
|
-
observer.observe(el)
|
|
67
|
-
el._observer = observer
|
|
68
|
-
},
|
|
69
|
-
unmounted(el) {
|
|
70
|
-
el._observer?.disconnect()
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Prefer Function Shorthand for Single-Hook Directives
|
|
76
|
-
|
|
77
|
-
If you only need `mounted`/`updated`, use the function form.
|
|
78
|
-
|
|
79
|
-
```ts
|
|
80
|
-
const vAutofocus = (el) => el.focus()
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Use the `v-` Prefix and Script Setup Registration
|
|
84
|
-
|
|
85
|
-
```vue
|
|
86
|
-
<script setup>
|
|
87
|
-
const vFocus = (el) => el.focus()
|
|
88
|
-
</script>
|
|
89
|
-
|
|
90
|
-
<template>
|
|
91
|
-
<input v-focus />
|
|
92
|
-
</template>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Type Custom Directives in TypeScript Projects
|
|
96
|
-
|
|
97
|
-
Use `Directive<Element, ValueType>` so `binding.value` is typed, and augment Vue's template types so directives are recognized in SFC templates.
|
|
98
|
-
|
|
99
|
-
**BAD:**
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
// Untyped directive value and no template type augmentation
|
|
103
|
-
export const vHighlight = {
|
|
104
|
-
mounted(el, binding) {
|
|
105
|
-
el.style.backgroundColor = binding.value
|
|
106
|
-
},
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**GOOD:**
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
import type { Directive } from 'vue'
|
|
114
|
-
|
|
115
|
-
type HighlightValue = string
|
|
116
|
-
|
|
117
|
-
export const vHighlight = {
|
|
118
|
-
mounted(el, binding) {
|
|
119
|
-
el.style.backgroundColor = binding.value
|
|
120
|
-
},
|
|
121
|
-
} satisfies Directive<HTMLElement, HighlightValue>
|
|
122
|
-
|
|
123
|
-
declare module 'vue' {
|
|
124
|
-
interface ComponentCustomProperties {
|
|
125
|
-
vHighlight: typeof vHighlight
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
## Handle SSR with `getSSRProps`
|
|
131
|
-
|
|
132
|
-
Directive hooks such as `mounted` and `updated` do not run during SSR. If a directive sets attributes/classes that affect rendered HTML, provide an SSR equivalent via `getSSRProps` to avoid hydration mismatches.
|
|
133
|
-
|
|
134
|
-
**BAD:**
|
|
135
|
-
|
|
136
|
-
```ts
|
|
137
|
-
const vTooltip = {
|
|
138
|
-
mounted(el, binding) {
|
|
139
|
-
el.setAttribute('data-tooltip', binding.value)
|
|
140
|
-
el.classList.add('has-tooltip')
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
**GOOD:**
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
const vTooltip = {
|
|
149
|
-
mounted(el, binding) {
|
|
150
|
-
el.setAttribute('data-tooltip', binding.value)
|
|
151
|
-
el.classList.add('has-tooltip')
|
|
152
|
-
},
|
|
153
|
-
getSSRProps(binding) {
|
|
154
|
-
return {
|
|
155
|
-
'data-tooltip': binding.value,
|
|
156
|
-
class: 'has-tooltip',
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Prefer Declarative Templates When Possible
|
|
163
|
-
|
|
164
|
-
If a standard attribute or binding works, use it instead of a directive.
|
|
165
|
-
|
|
166
|
-
## Decide Between Directives and Components
|
|
167
|
-
|
|
168
|
-
Use a directive for DOM-level behavior. Use a component when behavior affects structure, state, or rendering.
|