@sprlab/wccompiler 0.8.1 → 0.8.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/adapters/vue.js +63 -31
- package/integrations/vue.js +100 -6
- package/lib/codegen.js +28 -5
- package/package.json +1 -1
package/adapters/vue.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vue adapter for WCC defineModel — enables v-model and multi-model binding.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Setup (ONE line in main.js):
|
|
5
5
|
* import { createApp } from 'vue'
|
|
6
6
|
* import { wccVue } from '@sprlab/wccompiler/adapters/vue'
|
|
7
7
|
*
|
|
@@ -9,23 +9,29 @@
|
|
|
9
9
|
* app.use(wccVue) // registers adapter + v-wcc-model directive globally
|
|
10
10
|
* app.mount('#app')
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* IMPORTANT: Also use wccVuePlugin() in vite.config.js to enable v-model:propName
|
|
13
|
+
* on custom elements (via AST nodeTransform):
|
|
14
|
+
* import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
|
|
15
|
+
* export default { plugins: [wccVuePlugin()] }
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
* <!--
|
|
18
|
-
*
|
|
17
|
+
* With both configured, you can use:
|
|
18
|
+
* <!-- Native v-model:propName (preferred, requires wccVuePlugin) -->
|
|
19
|
+
* <wcc-input v-model:value="text"></wcc-input>
|
|
20
|
+
* <wcc-form v-model:count="countRef" v-model:title="titleRef"></wcc-form>
|
|
21
|
+
*
|
|
22
|
+
* <!-- v-model without argument (uses modelValue convention) -->
|
|
19
23
|
* <wcc-input v-model="text"></wcc-input>
|
|
20
24
|
*
|
|
21
|
-
* <!--
|
|
22
|
-
* <wcc-
|
|
25
|
+
* <!-- Fallback directive (for non-Vite setups without nodeTransform) -->
|
|
26
|
+
* <wcc-input v-wcc-model:value="textRef"></wcc-input>
|
|
23
27
|
*
|
|
24
28
|
* @module @sprlab/wccompiler/adapters/vue
|
|
25
29
|
*/
|
|
26
30
|
|
|
27
31
|
// ── Document-level adapter: wcc:model → update:propName ─────────────
|
|
28
32
|
// This enables Vue's native v-model on WCC custom elements.
|
|
33
|
+
// Vue v-model on custom elements listens for `update:modelValue` by default.
|
|
34
|
+
// The nodeTransform in wccVuePlugin makes v-model:propName listen for `update:propName`.
|
|
29
35
|
|
|
30
36
|
if (typeof document !== 'undefined') {
|
|
31
37
|
document.addEventListener('wcc:model', (e) => {
|
|
@@ -38,16 +44,20 @@ if (typeof document !== 'undefined') {
|
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
// ── Vue directive: v-wcc-model ──────────────────────────────────────
|
|
47
|
+
// Fallback for non-Vite setups. If using wccVuePlugin(), prefer v-model:propName instead.
|
|
48
|
+
//
|
|
49
|
+
// Usage:
|
|
50
|
+
// <wcc-input v-wcc-model:value="textRef"></wcc-input>
|
|
51
|
+
//
|
|
52
|
+
// The bound value MUST be a Vue ref (or reactive property).
|
|
53
|
+
// The directive writes directly to ref.value for WCC→Vue updates.
|
|
41
54
|
|
|
42
55
|
/**
|
|
43
56
|
* Vue custom directive for two-way binding with WCC defineModel props.
|
|
44
|
-
*
|
|
45
|
-
* Binds a Vue ref to a WCC component's model prop bidirectionally:
|
|
46
|
-
* - Parent → Child: sets the attribute when the Vue ref changes
|
|
47
|
-
* - Child → Parent: updates the Vue ref when wcc:model fires for the matching prop
|
|
57
|
+
* This is a FALLBACK — prefer v-model:propName with wccVuePlugin() nodeTransform.
|
|
48
58
|
*
|
|
49
59
|
* @example
|
|
50
|
-
* <wcc-counter v-wcc-model:count="
|
|
60
|
+
* <wcc-counter v-wcc-model:count="myCountRef"></wcc-counter>
|
|
51
61
|
*/
|
|
52
62
|
export const vWccModel = {
|
|
53
63
|
mounted(el, binding) {
|
|
@@ -58,31 +68,53 @@ export const vWccModel = {
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
// Set initial value (parent → child)
|
|
71
|
+
// Vue sets camelCase attributes, so set both camelCase and kebab-case
|
|
61
72
|
if (binding.value != null) {
|
|
62
73
|
el.setAttribute(propName, String(binding.value));
|
|
63
74
|
}
|
|
64
75
|
|
|
65
|
-
// Listen for child → parent changes
|
|
66
|
-
// (which is already dispatched by the document-level adapter above)
|
|
67
|
-
const handler = (e) => {
|
|
68
|
-
// Use the update:propName event dispatched by the adapter
|
|
69
|
-
// Vue will handle the ref update through the directive binding
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Listen directly for wcc:model to update the binding
|
|
76
|
+
// Listen for child → parent changes
|
|
73
77
|
const wccHandler = (e) => {
|
|
74
78
|
if (e.detail && e.detail.prop === propName) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
const newValue = e.detail.value;
|
|
80
|
+
|
|
81
|
+
// Try to update the Vue ref directly
|
|
82
|
+
// In Vue 3, if the binding expression is a ref, binding.value is the ref's current value
|
|
83
|
+
// We need to find the ref on the component instance and write to it
|
|
84
|
+
const instance = binding.instance;
|
|
85
|
+
if (instance) {
|
|
86
|
+
// Access the setup state to find the ref
|
|
87
|
+
const setupState = instance.$.setupState;
|
|
88
|
+
// The binding expression is stored in the directive's internal data
|
|
89
|
+
// In Vue 3, we can use the dir's exp to find the variable name
|
|
90
|
+
// Fallback: emit a custom event that a parent @update handler can catch
|
|
91
|
+
const refName = binding.dir?.__wccRefName?.[el]?.[propName];
|
|
92
|
+
if (refName && setupState[refName] !== undefined) {
|
|
93
|
+
// Direct ref write
|
|
94
|
+
if (setupState[refName]?.value !== undefined) {
|
|
95
|
+
setupState[refName].value = newValue;
|
|
96
|
+
} else {
|
|
97
|
+
setupState[refName] = newValue;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
// Fallback: try to find by matching current value
|
|
101
|
+
for (const key of Object.keys(setupState)) {
|
|
102
|
+
const val = setupState[key];
|
|
103
|
+
if (val?.value === binding.value || val === binding.value) {
|
|
104
|
+
if (val?.value !== undefined) {
|
|
105
|
+
val.value = newValue;
|
|
106
|
+
} else {
|
|
107
|
+
setupState[key] = newValue;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
81
114
|
}
|
|
82
115
|
};
|
|
83
116
|
|
|
84
117
|
el.addEventListener('wcc:model', wccHandler);
|
|
85
|
-
// Store handler for cleanup
|
|
86
118
|
el.__wccModelHandlers = el.__wccModelHandlers || {};
|
|
87
119
|
el.__wccModelHandlers[propName] = wccHandler;
|
|
88
120
|
},
|
|
@@ -103,7 +135,6 @@ export const vWccModel = {
|
|
|
103
135
|
const propName = binding.arg;
|
|
104
136
|
if (!propName) return;
|
|
105
137
|
|
|
106
|
-
// Cleanup listener
|
|
107
138
|
const handler = el.__wccModelHandlers?.[propName];
|
|
108
139
|
if (handler) {
|
|
109
140
|
el.removeEventListener('wcc:model', handler);
|
|
@@ -115,7 +146,8 @@ export const vWccModel = {
|
|
|
115
146
|
// ── Vue Plugin: app.use(wccVue) ─────────────────────────────────────
|
|
116
147
|
|
|
117
148
|
/**
|
|
118
|
-
* Vue plugin that registers the
|
|
149
|
+
* Vue plugin that registers the v-wcc-model directive globally.
|
|
150
|
+
* The document-level adapter is registered on import (side-effect above).
|
|
119
151
|
*
|
|
120
152
|
* @example
|
|
121
153
|
* import { createApp } from 'vue'
|
package/integrations/vue.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vue Vite plugin for WCC custom elements.
|
|
3
|
-
* Configures isCustomElement
|
|
3
|
+
* Configures isCustomElement and enables v-model:propName on custom elements.
|
|
4
4
|
*
|
|
5
5
|
* @module @sprlab/wccompiler/integrations/vue
|
|
6
6
|
*
|
|
7
7
|
* IMPORTANT: This file is for vite.config.js (Node.js context).
|
|
8
|
-
* For browser-side model adapter,
|
|
8
|
+
* For browser-side model adapter, use app.use(wccVue) from '@sprlab/wccompiler/adapters/vue'.
|
|
9
9
|
*
|
|
10
10
|
* @example vite.config.js
|
|
11
11
|
* ```js
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
* export default { plugins: [wccVuePlugin()] }
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
-
* @example main.js (browser — enables v-model
|
|
16
|
+
* @example main.js (browser — enables v-model event translation)
|
|
17
17
|
* ```js
|
|
18
|
-
* import '@sprlab/wccompiler/adapters/vue'
|
|
18
|
+
* import { wccVue } from '@sprlab/wccompiler/adapters/vue'
|
|
19
|
+
* app.use(wccVue)
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* With this plugin, v-model:propName works natively on WCC custom elements:
|
|
23
|
+
* ```vue
|
|
24
|
+
* <wcc-input v-model="text"></wcc-input>
|
|
25
|
+
* <wcc-form v-model:count="countRef" v-model:title="titleRef"></wcc-form>
|
|
19
26
|
* ```
|
|
20
27
|
*/
|
|
21
28
|
|
|
@@ -26,9 +33,95 @@ import vue from '@vitejs/plugin-vue'
|
|
|
26
33
|
* @property {string} [prefix='wcc-'] - Tag prefix for custom element detection
|
|
27
34
|
*/
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* AST node transform that enables v-model:propName on custom elements.
|
|
38
|
+
*
|
|
39
|
+
* Vue's compiler normally doesn't support v-model with arguments on custom elements.
|
|
40
|
+
* This transform intercepts v-model:arg directives on custom elements and rewrites them
|
|
41
|
+
* to the equivalent :prop + @update:prop binding that Vue understands.
|
|
42
|
+
*
|
|
43
|
+
* Transforms:
|
|
44
|
+
* <wcc-input v-model:value="text" />
|
|
45
|
+
* Into the equivalent of:
|
|
46
|
+
* <wcc-input :value="text" @update:value="text = $event" />
|
|
47
|
+
*
|
|
48
|
+
* @param {object} node - Vue compiler AST node
|
|
49
|
+
* @param {object} context - Vue compiler transform context
|
|
50
|
+
*/
|
|
51
|
+
function wccVModelTransform(node, context) {
|
|
52
|
+
// Only process element nodes (type 1 = ELEMENT)
|
|
53
|
+
if (node.type !== 1) return;
|
|
54
|
+
|
|
55
|
+
// Only process custom elements (tag contains a hyphen)
|
|
56
|
+
if (!node.tag.includes('-')) return;
|
|
57
|
+
|
|
58
|
+
// Find v-model directives with arguments
|
|
59
|
+
const newProps = [];
|
|
60
|
+
let modified = false;
|
|
61
|
+
|
|
62
|
+
for (const prop of node.props) {
|
|
63
|
+
// Check if this is a v-model directive with an argument
|
|
64
|
+
if (
|
|
65
|
+
prop.type === 7 && // DIRECTIVE
|
|
66
|
+
prop.name === 'model' &&
|
|
67
|
+
prop.arg // has argument (v-model:name)
|
|
68
|
+
) {
|
|
69
|
+
const propName = prop.arg.content;
|
|
70
|
+
const expr = prop.exp;
|
|
71
|
+
|
|
72
|
+
if (!expr) {
|
|
73
|
+
newProps.push(prop);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Replace v-model:propName="expr" with:
|
|
78
|
+
// :propName="expr" (bind directive)
|
|
79
|
+
newProps.push({
|
|
80
|
+
type: 7, // DIRECTIVE
|
|
81
|
+
name: 'bind',
|
|
82
|
+
arg: prop.arg,
|
|
83
|
+
exp: expr,
|
|
84
|
+
modifiers: [],
|
|
85
|
+
loc: prop.loc
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// @update:propName="$event => { expr = $event }" (on directive)
|
|
89
|
+
newProps.push({
|
|
90
|
+
type: 7, // DIRECTIVE
|
|
91
|
+
name: 'on',
|
|
92
|
+
arg: {
|
|
93
|
+
type: 4, // SIMPLE_EXPRESSION
|
|
94
|
+
content: `update:${propName}`,
|
|
95
|
+
isStatic: true,
|
|
96
|
+
constType: 3,
|
|
97
|
+
loc: prop.loc
|
|
98
|
+
},
|
|
99
|
+
exp: {
|
|
100
|
+
type: 4, // SIMPLE_EXPRESSION
|
|
101
|
+
content: `$event => { ${expr.content} = $event }`,
|
|
102
|
+
isStatic: false,
|
|
103
|
+
constType: 0,
|
|
104
|
+
loc: prop.loc
|
|
105
|
+
},
|
|
106
|
+
modifiers: [],
|
|
107
|
+
loc: prop.loc
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
modified = true;
|
|
111
|
+
} else {
|
|
112
|
+
newProps.push(prop);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (modified) {
|
|
117
|
+
node.props = newProps;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
29
121
|
/**
|
|
30
122
|
* Creates a Vite plugin that configures Vue's template compiler
|
|
31
|
-
* to recognize custom elements with the given prefix
|
|
123
|
+
* to recognize custom elements with the given prefix and enables
|
|
124
|
+
* v-model:propName on those elements.
|
|
32
125
|
*
|
|
33
126
|
* @param {WccVuePluginOptions} [options]
|
|
34
127
|
* @returns {import('vite').Plugin}
|
|
@@ -38,7 +131,8 @@ export function wccVuePlugin(options = {}) {
|
|
|
38
131
|
return vue({
|
|
39
132
|
template: {
|
|
40
133
|
compilerOptions: {
|
|
41
|
-
isCustomElement: (tag) => tag.startsWith(prefix)
|
|
134
|
+
isCustomElement: (tag) => tag.startsWith(prefix),
|
|
135
|
+
nodeTransforms: [wccVModelTransform]
|
|
42
136
|
}
|
|
43
137
|
}
|
|
44
138
|
})
|
package/lib/codegen.js
CHANGED
|
@@ -945,7 +945,18 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
945
945
|
const modelAttrNames = modelDefs.map(md => camelToKebab(md.name));
|
|
946
946
|
if (propDefs.length > 0 || modelDefs.length > 0) {
|
|
947
947
|
const propAttrNames = propDefs.map(p => `'${p.attrName}'`);
|
|
948
|
-
|
|
948
|
+
// For model props, observe BOTH kebab-case AND camelCase forms
|
|
949
|
+
// Vue sets camelCase (modelValue), native HTML uses kebab-case (model-value)
|
|
950
|
+
const modelAttrEntries = [];
|
|
951
|
+
for (let i = 0; i < modelDefs.length; i++) {
|
|
952
|
+
const kebab = modelAttrNames[i];
|
|
953
|
+
const camel = modelDefs[i].name;
|
|
954
|
+
modelAttrEntries.push(`'${kebab}'`);
|
|
955
|
+
// Only add camelCase if it differs from kebab-case
|
|
956
|
+
if (kebab !== camel) {
|
|
957
|
+
modelAttrEntries.push(`'${camel}'`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
949
960
|
const allAttrNames = [...propAttrNames, ...modelAttrEntries].join(', ');
|
|
950
961
|
lines.push(` static get observedAttributes() { return [${allAttrNames}]; }`);
|
|
951
962
|
lines.push('');
|
|
@@ -1149,8 +1160,11 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1149
1160
|
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
1150
1161
|
lines.push(' }));');
|
|
1151
1162
|
} else if (b.type === 'signal') {
|
|
1163
|
+
// Check if this is a model var (needs _m_ prefix instead of _)
|
|
1164
|
+
const modelPropName = modelVarMap.get(b.name);
|
|
1165
|
+
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1152
1166
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1153
|
-
lines.push(` this.${b.varName}.textContent =
|
|
1167
|
+
lines.push(` this.${b.varName}.textContent = ${signalRef} ?? '';`);
|
|
1154
1168
|
lines.push(' }));');
|
|
1155
1169
|
} else if (b.type === 'computed') {
|
|
1156
1170
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
@@ -1201,7 +1215,8 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1201
1215
|
} else if (pb.type === 'computed') {
|
|
1202
1216
|
ref = `this._c_${pb.expr}()`;
|
|
1203
1217
|
} else if (pb.type === 'signal') {
|
|
1204
|
-
|
|
1218
|
+
const modelPropName = modelVarMap.get(pb.expr);
|
|
1219
|
+
ref = modelPropName ? `this._m_${modelPropName}()` : `this._${pb.expr}()`;
|
|
1205
1220
|
} else if (pb.type === 'constant') {
|
|
1206
1221
|
ref = `this._const_${pb.expr}`;
|
|
1207
1222
|
} else {
|
|
@@ -1595,6 +1610,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1595
1610
|
for (let i = 0; i < modelDefs.length; i++) {
|
|
1596
1611
|
const md = modelDefs[i];
|
|
1597
1612
|
const attrName = modelAttrNames[i];
|
|
1613
|
+
const camelName = md.name;
|
|
1598
1614
|
const defaultVal = md.default;
|
|
1599
1615
|
let updateExpr;
|
|
1600
1616
|
|
|
@@ -1612,7 +1628,12 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1612
1628
|
updateExpr = `this._m_${md.name}(newVal ?? ${defaultVal})`;
|
|
1613
1629
|
}
|
|
1614
1630
|
|
|
1615
|
-
|
|
1631
|
+
// Handle both kebab-case (native HTML) and camelCase (Vue) attribute names
|
|
1632
|
+
if (attrName !== camelName) {
|
|
1633
|
+
lines.push(` if (name === '${attrName}' || name === '${camelName}') ${updateExpr};`);
|
|
1634
|
+
} else {
|
|
1635
|
+
lines.push(` if (name === '${attrName}') ${updateExpr};`);
|
|
1636
|
+
}
|
|
1616
1637
|
}
|
|
1617
1638
|
|
|
1618
1639
|
lines.push(' }');
|
|
@@ -1725,7 +1746,9 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1725
1746
|
if (b.type === 'prop') {
|
|
1726
1747
|
lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
|
|
1727
1748
|
} else if (b.type === 'signal') {
|
|
1728
|
-
|
|
1749
|
+
const modelPropName = modelVarMap.get(b.name);
|
|
1750
|
+
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1751
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = ${signalRef} ?? ''; });`);
|
|
1729
1752
|
} else if (b.type === 'computed') {
|
|
1730
1753
|
lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
|
|
1731
1754
|
} else {
|
package/package.json
CHANGED