ima-claude 2.9.0
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/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# Complete Component Examples
|
|
2
|
+
|
|
3
|
+
Full working examples demonstrating Vue FP patterns.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Product Card Component](#product-card-component)
|
|
8
|
+
2. [User Dashboard with Wrapper](#user-dashboard-with-wrapper)
|
|
9
|
+
3. [Form with Validation](#form-with-validation)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Product Card Component
|
|
14
|
+
|
|
15
|
+
Complete example with pure composable, component, and styling.
|
|
16
|
+
|
|
17
|
+
```vue
|
|
18
|
+
<!-- ProductCard.vue -->
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import type { Ref } from 'vue'
|
|
21
|
+
import { computed, readonly, toRef } from 'vue'
|
|
22
|
+
|
|
23
|
+
interface Product {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
price: number
|
|
27
|
+
inStock: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ───── Pure composable ─────
|
|
31
|
+
const useProductLogic = (product: Readonly<Ref<Product>>) => {
|
|
32
|
+
const displayData = computed(() => ({
|
|
33
|
+
...product.value,
|
|
34
|
+
formattedPrice: `$${product.value.price.toFixed(2)}`,
|
|
35
|
+
availability: product.value.inStock ? 'In Stock' : 'Out of Stock'
|
|
36
|
+
}))
|
|
37
|
+
|
|
38
|
+
const cssClasses = computed(() => ({
|
|
39
|
+
card: `product-card ${product.value.inStock ? 'in-stock' : 'out-of-stock'}`,
|
|
40
|
+
price: `product-price ${product.value.inStock ? 'available' : 'unavailable'}`
|
|
41
|
+
}))
|
|
42
|
+
|
|
43
|
+
return { displayData, cssClasses }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ───── Component setup ─────
|
|
47
|
+
const props = defineProps<{
|
|
48
|
+
product: Product
|
|
49
|
+
}>()
|
|
50
|
+
|
|
51
|
+
const emit = defineEmits<{
|
|
52
|
+
addToCart: [productId: string]
|
|
53
|
+
}>()
|
|
54
|
+
|
|
55
|
+
const { displayData, cssClasses } = useProductLogic(
|
|
56
|
+
readonly(toRef(props, 'product'))
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const handleAddToCart = () => {
|
|
60
|
+
if (props.product.inStock) {
|
|
61
|
+
emit('addToCart', props.product.id)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<div :class="cssClasses.card">
|
|
68
|
+
<h3>{{ displayData.name }}</h3>
|
|
69
|
+
<p :class="cssClasses.price">{{ displayData.formattedPrice }}</p>
|
|
70
|
+
<p class="availability">{{ displayData.availability }}</p>
|
|
71
|
+
<button
|
|
72
|
+
@click="handleAddToCart"
|
|
73
|
+
:disabled="!product.inStock"
|
|
74
|
+
>
|
|
75
|
+
Add to Cart
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<style scoped>
|
|
81
|
+
.product-card {
|
|
82
|
+
border: 1px solid #ddd;
|
|
83
|
+
padding: 1rem;
|
|
84
|
+
border-radius: 8px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.product-card.in-stock {
|
|
88
|
+
border-color: #28a745;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.product-card.out-of-stock {
|
|
92
|
+
opacity: 0.6;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.product-price.unavailable {
|
|
96
|
+
text-decoration: line-through;
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## User Dashboard with Wrapper
|
|
104
|
+
|
|
105
|
+
Pure component + wrapper pattern with API integration.
|
|
106
|
+
|
|
107
|
+
### Pure Component
|
|
108
|
+
|
|
109
|
+
```vue
|
|
110
|
+
<!-- UserListPure.vue -->
|
|
111
|
+
<script setup lang="ts">
|
|
112
|
+
interface User {
|
|
113
|
+
id: string
|
|
114
|
+
name: string
|
|
115
|
+
email: string
|
|
116
|
+
role: 'admin' | 'user'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
defineProps<{
|
|
120
|
+
users: User[]
|
|
121
|
+
selectedId: string | null
|
|
122
|
+
}>()
|
|
123
|
+
|
|
124
|
+
const emit = defineEmits<{
|
|
125
|
+
select: [userId: string]
|
|
126
|
+
delete: [userId: string]
|
|
127
|
+
}>()
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
<template>
|
|
131
|
+
<ul class="user-list">
|
|
132
|
+
<li
|
|
133
|
+
v-for="user in users"
|
|
134
|
+
:key="user.id"
|
|
135
|
+
:class="{ selected: user.id === selectedId }"
|
|
136
|
+
@click="emit('select', user.id)"
|
|
137
|
+
>
|
|
138
|
+
<span class="name">{{ user.name }}</span>
|
|
139
|
+
<span class="email">{{ user.email }}</span>
|
|
140
|
+
<span :class="['role', user.role]">{{ user.role }}</span>
|
|
141
|
+
<button @click.stop="emit('delete', user.id)">Delete</button>
|
|
142
|
+
</li>
|
|
143
|
+
</ul>
|
|
144
|
+
</template>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Wrapper Component
|
|
148
|
+
|
|
149
|
+
```vue
|
|
150
|
+
<!-- UserDashboard.vue -->
|
|
151
|
+
<script setup lang="ts">
|
|
152
|
+
import { ref, onMounted } from 'vue'
|
|
153
|
+
import UserListPure from './UserListPure.vue'
|
|
154
|
+
|
|
155
|
+
interface User {
|
|
156
|
+
id: string
|
|
157
|
+
name: string
|
|
158
|
+
email: string
|
|
159
|
+
role: 'admin' | 'user'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Injected or imported API
|
|
163
|
+
const userApi = {
|
|
164
|
+
getUsers: async (): Promise<User[]> => {
|
|
165
|
+
const response = await fetch('/api/users')
|
|
166
|
+
return response.json()
|
|
167
|
+
},
|
|
168
|
+
deleteUser: async (id: string): Promise<void> => {
|
|
169
|
+
await fetch(`/api/users/${id}`, { method: 'DELETE' })
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// State
|
|
174
|
+
const users = ref<User[]>([])
|
|
175
|
+
const selectedId = ref<string | null>(null)
|
|
176
|
+
const loading = ref(true)
|
|
177
|
+
const error = ref<Error | null>(null)
|
|
178
|
+
|
|
179
|
+
// Side effects isolated to wrapper
|
|
180
|
+
const fetchUsers = async () => {
|
|
181
|
+
loading.value = true
|
|
182
|
+
error.value = null
|
|
183
|
+
try {
|
|
184
|
+
users.value = await userApi.getUsers()
|
|
185
|
+
} catch (e) {
|
|
186
|
+
error.value = e as Error
|
|
187
|
+
} finally {
|
|
188
|
+
loading.value = false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const handleSelect = (userId: string) => {
|
|
193
|
+
selectedId.value = userId
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const handleDelete = async (userId: string) => {
|
|
197
|
+
try {
|
|
198
|
+
await userApi.deleteUser(userId)
|
|
199
|
+
await fetchUsers() // Refresh list
|
|
200
|
+
if (selectedId.value === userId) {
|
|
201
|
+
selectedId.value = null
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
error.value = e as Error
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onMounted(fetchUsers)
|
|
209
|
+
</script>
|
|
210
|
+
|
|
211
|
+
<template>
|
|
212
|
+
<div class="user-dashboard">
|
|
213
|
+
<h2>Users</h2>
|
|
214
|
+
|
|
215
|
+
<div v-if="loading" class="loading">Loading users...</div>
|
|
216
|
+
|
|
217
|
+
<div v-else-if="error" class="error">
|
|
218
|
+
Error: {{ error.message }}
|
|
219
|
+
<button @click="fetchUsers">Retry</button>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<UserListPure
|
|
223
|
+
v-else
|
|
224
|
+
:users="users"
|
|
225
|
+
:selected-id="selectedId"
|
|
226
|
+
@select="handleSelect"
|
|
227
|
+
@delete="handleDelete"
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
</template>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Form with Validation
|
|
236
|
+
|
|
237
|
+
Pure validation logic with form component.
|
|
238
|
+
|
|
239
|
+
### Validation Composable
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// composables/useFormValidation.ts
|
|
243
|
+
import type { Ref } from 'vue'
|
|
244
|
+
import { computed, ref } from 'vue'
|
|
245
|
+
|
|
246
|
+
type Validator<T> = (value: T) => string | null
|
|
247
|
+
|
|
248
|
+
interface FieldConfig<T> {
|
|
249
|
+
value: Ref<T>
|
|
250
|
+
validators: Validator<T>[]
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export const useFormValidation = <T extends Record<string, unknown>>(
|
|
254
|
+
fields: { [K in keyof T]: FieldConfig<T[K]> }
|
|
255
|
+
) => {
|
|
256
|
+
const touched = ref<Record<keyof T, boolean>>(
|
|
257
|
+
Object.keys(fields).reduce((acc, key) => ({ ...acc, [key]: false }), {} as Record<keyof T, boolean>)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const errors = computed(() => {
|
|
261
|
+
const result: Partial<Record<keyof T, string>> = {}
|
|
262
|
+
|
|
263
|
+
for (const [key, config] of Object.entries(fields) as [keyof T, FieldConfig<T[keyof T]>][]) {
|
|
264
|
+
for (const validator of config.validators) {
|
|
265
|
+
const error = validator(config.value.value)
|
|
266
|
+
if (error) {
|
|
267
|
+
result[key] = error
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return result
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const isValid = computed(() => Object.keys(errors.value).length === 0)
|
|
277
|
+
|
|
278
|
+
const touch = (field: keyof T) => {
|
|
279
|
+
touched.value[field] = true
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const touchAll = () => {
|
|
283
|
+
for (const key of Object.keys(fields)) {
|
|
284
|
+
touched.value[key as keyof T] = true
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const getError = (field: keyof T) => {
|
|
289
|
+
return touched.value[field] ? errors.value[field] : null
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { errors, isValid, touched, touch, touchAll, getError }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Common validators
|
|
296
|
+
export const required = (msg = 'Required'): Validator<string> =>
|
|
297
|
+
(value) => value.trim() ? null : msg
|
|
298
|
+
|
|
299
|
+
export const minLength = (min: number, msg?: string): Validator<string> =>
|
|
300
|
+
(value) => value.length >= min ? null : msg ?? `Min ${min} characters`
|
|
301
|
+
|
|
302
|
+
export const email = (msg = 'Invalid email'): Validator<string> =>
|
|
303
|
+
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : msg
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Form Component
|
|
307
|
+
|
|
308
|
+
```vue
|
|
309
|
+
<!-- UserForm.vue -->
|
|
310
|
+
<script setup lang="ts">
|
|
311
|
+
import { ref } from 'vue'
|
|
312
|
+
import { useFormValidation, required, minLength, email } from '@/composables/useFormValidation'
|
|
313
|
+
|
|
314
|
+
const emit = defineEmits<{
|
|
315
|
+
submit: [data: { name: string; email: string }]
|
|
316
|
+
}>()
|
|
317
|
+
|
|
318
|
+
// Form state
|
|
319
|
+
const name = ref('')
|
|
320
|
+
const userEmail = ref('')
|
|
321
|
+
|
|
322
|
+
// Validation
|
|
323
|
+
const { isValid, touch, touchAll, getError } = useFormValidation({
|
|
324
|
+
name: {
|
|
325
|
+
value: name,
|
|
326
|
+
validators: [required(), minLength(2)]
|
|
327
|
+
},
|
|
328
|
+
email: {
|
|
329
|
+
value: userEmail,
|
|
330
|
+
validators: [required(), email()]
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const handleSubmit = () => {
|
|
335
|
+
touchAll()
|
|
336
|
+
if (isValid.value) {
|
|
337
|
+
emit('submit', { name: name.value, email: userEmail.value })
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
</script>
|
|
341
|
+
|
|
342
|
+
<template>
|
|
343
|
+
<form @submit.prevent="handleSubmit" class="user-form">
|
|
344
|
+
<div class="field">
|
|
345
|
+
<label for="name">Name</label>
|
|
346
|
+
<input
|
|
347
|
+
id="name"
|
|
348
|
+
v-model="name"
|
|
349
|
+
@blur="touch('name')"
|
|
350
|
+
:class="{ error: getError('name') }"
|
|
351
|
+
/>
|
|
352
|
+
<span v-if="getError('name')" class="error-msg">{{ getError('name') }}</span>
|
|
353
|
+
</div>
|
|
354
|
+
|
|
355
|
+
<div class="field">
|
|
356
|
+
<label for="email">Email</label>
|
|
357
|
+
<input
|
|
358
|
+
id="email"
|
|
359
|
+
type="email"
|
|
360
|
+
v-model="userEmail"
|
|
361
|
+
@blur="touch('email')"
|
|
362
|
+
:class="{ error: getError('email') }"
|
|
363
|
+
/>
|
|
364
|
+
<span v-if="getError('email')" class="error-msg">{{ getError('email') }}</span>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<button type="submit" :disabled="!isValid">Submit</button>
|
|
368
|
+
</form>
|
|
369
|
+
</template>
|
|
370
|
+
|
|
371
|
+
<style scoped>
|
|
372
|
+
.user-form .field {
|
|
373
|
+
margin-bottom: 1rem;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.user-form input.error {
|
|
377
|
+
border-color: #dc3545;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.user-form .error-msg {
|
|
381
|
+
color: #dc3545;
|
|
382
|
+
font-size: 0.875rem;
|
|
383
|
+
}
|
|
384
|
+
</style>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Pattern Summary
|
|
390
|
+
|
|
391
|
+
| Pattern | Example | Benefit |
|
|
392
|
+
|---------|---------|---------|
|
|
393
|
+
| Pure Composable | `useProductLogic` | 100% testable logic |
|
|
394
|
+
| Wrapper Pattern | `UserDashboard` + `UserListPure` | Isolated side effects |
|
|
395
|
+
| Validation Composable | `useFormValidation` | Reusable, pure validation |
|
|
396
|
+
| Computed CSS | `cssClasses` | Cached class computation |
|
|
397
|
+
| Event Delegation | `emit('select')` | Unidirectional data flow |
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Composables Advanced Patterns
|
|
2
|
+
|
|
3
|
+
Advanced composable patterns for Vue.js FP architecture.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Composable Factory Pattern](#composable-factory-pattern)
|
|
8
|
+
2. [State Management with Composables](#state-management-with-composables)
|
|
9
|
+
3. [Provide/Inject for Dependency Injection](#provideinject-for-dependency-injection)
|
|
10
|
+
4. [Lifecycle Integration](#lifecycle-integration)
|
|
11
|
+
5. [Composable Composition](#composable-composition)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Composable Factory Pattern
|
|
16
|
+
|
|
17
|
+
Pre-compile expensive configurations for performance.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// composables/useUserFactory.ts
|
|
21
|
+
import type { Ref } from 'vue'
|
|
22
|
+
import { computed } from 'vue'
|
|
23
|
+
|
|
24
|
+
interface UserData {
|
|
25
|
+
id: string
|
|
26
|
+
name: string
|
|
27
|
+
email: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface UserConfig {
|
|
31
|
+
showEmail: boolean
|
|
32
|
+
variant: 'compact' | 'detailed'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Factory creates optimized composable
|
|
36
|
+
const createUserComposable = (defaultConfig: UserConfig) => {
|
|
37
|
+
// Pre-compile static configuration (hot path optimization)
|
|
38
|
+
const compiledClasses = {
|
|
39
|
+
compact: {
|
|
40
|
+
card: 'user-card user-card--compact',
|
|
41
|
+
name: 'user-card__name user-card__name--compact'
|
|
42
|
+
},
|
|
43
|
+
detailed: {
|
|
44
|
+
card: 'user-card user-card--detailed',
|
|
45
|
+
name: 'user-card__name user-card__name--detailed'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (userData: Readonly<Ref<UserData>>, config?: Partial<UserConfig>) => {
|
|
50
|
+
const mergedConfig = { ...defaultConfig, ...config }
|
|
51
|
+
|
|
52
|
+
const displayData = computed(() => ({
|
|
53
|
+
...userData.value,
|
|
54
|
+
displayName: userData.value.name.trim()
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
// O(1) lookup instead of string concatenation
|
|
58
|
+
const cssClasses = computed(() => compiledClasses[mergedConfig.variant])
|
|
59
|
+
|
|
60
|
+
return { displayData, cssClasses }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Usage: Pre-compile for different contexts
|
|
65
|
+
export const useCompactUser = createUserComposable({
|
|
66
|
+
showEmail: false,
|
|
67
|
+
variant: 'compact'
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const useDetailedUser = createUserComposable({
|
|
71
|
+
showEmail: true,
|
|
72
|
+
variant: 'detailed'
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## State Management with Composables
|
|
79
|
+
|
|
80
|
+
Use composables for 95% of state management. Only use Pinia/Vuex for truly global state.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// composables/useUserState.ts
|
|
84
|
+
import { ref, readonly } from 'vue'
|
|
85
|
+
|
|
86
|
+
interface UserData {
|
|
87
|
+
id: string
|
|
88
|
+
name: string
|
|
89
|
+
email: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const useUserState = () => {
|
|
93
|
+
const users = ref<UserData[]>([])
|
|
94
|
+
const loading = ref(false)
|
|
95
|
+
|
|
96
|
+
// Pure functions for state transitions
|
|
97
|
+
const addUser = (user: UserData) => {
|
|
98
|
+
users.value = [...users.value, user] // Immutable
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const updateUser = (userId: string, updates: Partial<UserData>) => {
|
|
102
|
+
users.value = users.value.map(user =>
|
|
103
|
+
user.id === userId ? { ...user, ...updates } : user
|
|
104
|
+
) // Immutable
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const removeUser = (userId: string) => {
|
|
108
|
+
users.value = users.value.filter(user => user.id !== userId) // Immutable
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Side effect isolated
|
|
112
|
+
const fetchUsers = async (userApi: { getUsers: () => Promise<UserData[]> }) => {
|
|
113
|
+
loading.value = true
|
|
114
|
+
try {
|
|
115
|
+
users.value = await userApi.getUsers()
|
|
116
|
+
} finally {
|
|
117
|
+
loading.value = false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
users: readonly(users),
|
|
123
|
+
loading: readonly(loading),
|
|
124
|
+
addUser,
|
|
125
|
+
updateUser,
|
|
126
|
+
removeUser,
|
|
127
|
+
fetchUsers
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Provide/Inject for Dependency Injection
|
|
135
|
+
|
|
136
|
+
Use Vue's provide/inject for cross-component dependency injection.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// composables/useApiProvider.ts
|
|
140
|
+
import { provide, inject, type InjectionKey } from 'vue'
|
|
141
|
+
|
|
142
|
+
interface ApiClient {
|
|
143
|
+
getUser: (id: string) => Promise<UserData>
|
|
144
|
+
updateUser: (user: UserData) => Promise<void>
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Type-safe injection key
|
|
148
|
+
const API_KEY: InjectionKey<ApiClient> = Symbol('api')
|
|
149
|
+
|
|
150
|
+
// Provider composable (use in root component)
|
|
151
|
+
export const useApiProvider = (client: ApiClient) => {
|
|
152
|
+
provide(API_KEY, client)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Consumer composable (use in child components)
|
|
156
|
+
export const useApi = (): ApiClient => {
|
|
157
|
+
const api = inject(API_KEY)
|
|
158
|
+
if (!api) {
|
|
159
|
+
throw new Error('useApi must be used within ApiProvider')
|
|
160
|
+
}
|
|
161
|
+
return api
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Mock provider for testing
|
|
165
|
+
export const useMockApiProvider = () => {
|
|
166
|
+
const mockClient: ApiClient = {
|
|
167
|
+
getUser: async (id) => ({ id, name: 'Mock User', email: 'mock@test.com' }),
|
|
168
|
+
updateUser: async () => {}
|
|
169
|
+
}
|
|
170
|
+
provide(API_KEY, mockClient)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Lifecycle Integration
|
|
177
|
+
|
|
178
|
+
Integrate composables with Vue lifecycle hooks.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// composables/usePolling.ts
|
|
182
|
+
import { ref, readonly, onMounted, onUnmounted } from 'vue'
|
|
183
|
+
|
|
184
|
+
export const usePolling = <T>(
|
|
185
|
+
fetcher: () => Promise<T>,
|
|
186
|
+
intervalMs: number = 60000
|
|
187
|
+
) => {
|
|
188
|
+
const data = ref<T | null>(null)
|
|
189
|
+
const error = ref<Error | null>(null)
|
|
190
|
+
const loading = ref(false)
|
|
191
|
+
|
|
192
|
+
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
193
|
+
|
|
194
|
+
const fetch = async () => {
|
|
195
|
+
loading.value = true
|
|
196
|
+
try {
|
|
197
|
+
data.value = await fetcher()
|
|
198
|
+
error.value = null
|
|
199
|
+
} catch (e) {
|
|
200
|
+
error.value = e as Error
|
|
201
|
+
} finally {
|
|
202
|
+
loading.value = false
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const start = () => {
|
|
207
|
+
fetch() // Initial fetch
|
|
208
|
+
intervalId = setInterval(fetch, intervalMs)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const stop = () => {
|
|
212
|
+
if (intervalId) {
|
|
213
|
+
clearInterval(intervalId)
|
|
214
|
+
intervalId = null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Lifecycle integration
|
|
219
|
+
onMounted(start)
|
|
220
|
+
onUnmounted(stop)
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
data: readonly(data),
|
|
224
|
+
error: readonly(error),
|
|
225
|
+
loading: readonly(loading),
|
|
226
|
+
refetch: fetch,
|
|
227
|
+
stop
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Composable Composition
|
|
235
|
+
|
|
236
|
+
Compose multiple composables together.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// composables/useUserDashboard.ts
|
|
240
|
+
import { computed } from 'vue'
|
|
241
|
+
import { useUserState } from './useUserState'
|
|
242
|
+
import { usePolling } from './usePolling'
|
|
243
|
+
|
|
244
|
+
export const useUserDashboard = (userApi: { getUsers: () => Promise<UserData[]> }) => {
|
|
245
|
+
// Compose state management
|
|
246
|
+
const { users, loading: stateLoading, addUser, updateUser, removeUser } = useUserState()
|
|
247
|
+
|
|
248
|
+
// Compose polling
|
|
249
|
+
const { data: freshUsers, loading: pollLoading, refetch } = usePolling(
|
|
250
|
+
userApi.getUsers,
|
|
251
|
+
30000
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
// Derived state from composed composables
|
|
255
|
+
const isLoading = computed(() => stateLoading.value || pollLoading.value)
|
|
256
|
+
const userCount = computed(() => users.value.length)
|
|
257
|
+
const hasUsers = computed(() => userCount.value > 0)
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
users,
|
|
261
|
+
isLoading,
|
|
262
|
+
userCount,
|
|
263
|
+
hasUsers,
|
|
264
|
+
addUser,
|
|
265
|
+
updateUser,
|
|
266
|
+
removeUser,
|
|
267
|
+
refetch
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## When to Use These Patterns
|
|
275
|
+
|
|
276
|
+
| Pattern | Use When |
|
|
277
|
+
|---------|----------|
|
|
278
|
+
| Factory | Multiple similar composables with different configs |
|
|
279
|
+
| State Management | Local/feature state (95% of cases) |
|
|
280
|
+
| Provide/Inject | Cross-component dependencies, testing |
|
|
281
|
+
| Lifecycle Integration | Timers, subscriptions, cleanup needed |
|
|
282
|
+
| Composition | Building complex features from simple parts |
|