@sprlab/wccompiler 0.10.12 → 0.11.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/adapters/angular.ts +81 -0
- package/adapters/vue.js +9 -8
- package/integrations/angular.js +2 -2
- package/integrations/vue.js +13 -15
- package/lib/codegen.js +6 -33
- package/package.json +1 -1
package/adapters/angular.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - WccSlotsDirective: Main directive activated via [wccSlots] attribute
|
|
7
7
|
* - WccEvent: Single-event directive (wccEvent="name" + wccEmit output)
|
|
8
8
|
* - WccEvents: Multi-event bridging directive (kebab-case → camelCase)
|
|
9
|
+
* - WccModel: Two-way binding bridge for [(prop)] banana-box syntax
|
|
9
10
|
* - SlotContext: Interface for template context typing
|
|
10
11
|
*
|
|
11
12
|
* Usage:
|
|
@@ -547,3 +548,83 @@ export class WccEvents implements OnInit, OnDestroy {
|
|
|
547
548
|
this.listeners = [];
|
|
548
549
|
}
|
|
549
550
|
}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
// ─── WccModel — Two-way Binding Bridge (OPTIONAL) ───────────────────────────
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Optional directive for Angular's [(prop)] banana-box syntax on WCC elements.
|
|
557
|
+
*
|
|
558
|
+
* NOTE: As of WCC v0.11+, the compiled component emits `propChange` directly,
|
|
559
|
+
* so [(prop)] works zero-config without this directive. This directive is kept
|
|
560
|
+
* as an alternative that uses the structured wcc:model event instead.
|
|
561
|
+
*
|
|
562
|
+
* Angular's [(prop)] expands to:
|
|
563
|
+
* [prop]="value" (propChange)="value = $event.detail"
|
|
564
|
+
*
|
|
565
|
+
* The component already emits `propChange` natively, so this works out of the box.
|
|
566
|
+
* This directive provides an alternative path via wcc:model for advanced use cases
|
|
567
|
+
* (e.g., when you need access to oldValue or want to handle multiple models centrally).
|
|
568
|
+
*
|
|
569
|
+
* Usage (optional):
|
|
570
|
+
* <wcc-input wccModel [(value)]="text"></wcc-input>
|
|
571
|
+
*/
|
|
572
|
+
@Directive({
|
|
573
|
+
selector: '[wccModel]',
|
|
574
|
+
standalone: true,
|
|
575
|
+
})
|
|
576
|
+
export class WccModel implements OnInit, OnDestroy {
|
|
577
|
+
/** Optional explicit list of model prop names to bridge */
|
|
578
|
+
@Input() wccModel: string[] | '' = '';
|
|
579
|
+
|
|
580
|
+
private el = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
581
|
+
private listener: ((e: Event) => void) | null = null;
|
|
582
|
+
|
|
583
|
+
ngOnInit(): void {
|
|
584
|
+
const hostEl = this.el.nativeElement;
|
|
585
|
+
const tagName = hostEl.tagName.toLowerCase();
|
|
586
|
+
if (!tagName.includes('-')) return;
|
|
587
|
+
|
|
588
|
+
this.setupModelBridge(hostEl, tagName);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private async setupModelBridge(hostEl: HTMLElement, tagName: string): Promise<void> {
|
|
592
|
+
// Determine which model props to bridge
|
|
593
|
+
let modelNames: string[];
|
|
594
|
+
|
|
595
|
+
if (Array.isArray(this.wccModel) && this.wccModel.length > 0) {
|
|
596
|
+
modelNames = this.wccModel;
|
|
597
|
+
} else {
|
|
598
|
+
// Auto-discover from component metadata
|
|
599
|
+
await customElements.whenDefined(tagName);
|
|
600
|
+
const ctor = customElements.get(tagName) as any;
|
|
601
|
+
modelNames = ctor?.__meta?.models || [];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (modelNames.length === 0) return;
|
|
605
|
+
|
|
606
|
+
const modelSet = new Set(modelNames);
|
|
607
|
+
|
|
608
|
+
// Listen for wcc:model and re-dispatch as propChange
|
|
609
|
+
this.listener = (e: Event) => {
|
|
610
|
+
const detail = (e as CustomEvent).detail;
|
|
611
|
+
if (!detail || !modelSet.has(detail.prop)) return;
|
|
612
|
+
|
|
613
|
+
// Dispatch propChange (Angular banana-box convention)
|
|
614
|
+
hostEl.dispatchEvent(new CustomEvent(`${detail.prop}Change`, {
|
|
615
|
+
detail: detail.value,
|
|
616
|
+
bubbles: false,
|
|
617
|
+
cancelable: false,
|
|
618
|
+
}));
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
hostEl.addEventListener('wcc:model', this.listener);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
ngOnDestroy(): void {
|
|
625
|
+
if (this.listener) {
|
|
626
|
+
this.el.nativeElement.removeEventListener('wcc:model', this.listener);
|
|
627
|
+
this.listener = null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
package/adapters/vue.js
CHANGED
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
// ── Vue directive: v-wcc-model ──────────────────────────────────────
|
|
23
|
-
// Fallback for non-Vite setups. Listens for
|
|
23
|
+
// Fallback for non-Vite setups. Listens for wcc:model and filters by prop name.
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Vue custom directive for two-way binding with WCC defineModel props.
|
|
27
|
-
* Listens for `
|
|
27
|
+
* Listens for `wcc:model` CustomEvent and filters by prop name.
|
|
28
28
|
*/
|
|
29
29
|
export const vWccModel = {
|
|
30
30
|
mounted(el, binding) {
|
|
@@ -39,9 +39,10 @@ export const vWccModel = {
|
|
|
39
39
|
el.setAttribute(propName, String(binding.value));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Listen for
|
|
43
|
-
const kebabName = propName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
42
|
+
// Listen for wcc:model (WCC → Vue), filter by prop name
|
|
44
43
|
const handler = (e) => {
|
|
44
|
+
if (e.detail?.prop !== propName) return;
|
|
45
|
+
const newVal = e.detail.value;
|
|
45
46
|
// Try to update the Vue ref via setupState
|
|
46
47
|
const instance = binding.instance;
|
|
47
48
|
if (instance) {
|
|
@@ -51,9 +52,9 @@ export const vWccModel = {
|
|
|
51
52
|
const val = setupState[key];
|
|
52
53
|
if (val === binding.value || val?.value === binding.value) {
|
|
53
54
|
if (val?.value !== undefined) {
|
|
54
|
-
val.value =
|
|
55
|
+
val.value = newVal;
|
|
55
56
|
} else {
|
|
56
|
-
setupState[key] =
|
|
57
|
+
setupState[key] = newVal;
|
|
57
58
|
}
|
|
58
59
|
break;
|
|
59
60
|
}
|
|
@@ -61,9 +62,9 @@ export const vWccModel = {
|
|
|
61
62
|
}
|
|
62
63
|
};
|
|
63
64
|
|
|
64
|
-
el.addEventListener(
|
|
65
|
+
el.addEventListener('wcc:model', handler);
|
|
65
66
|
el.__wccModelHandlers = el.__wccModelHandlers || {};
|
|
66
|
-
el.__wccModelHandlers[propName] = { handler, eventName:
|
|
67
|
+
el.__wccModelHandlers[propName] = { handler, eventName: 'wcc:model' };
|
|
67
68
|
},
|
|
68
69
|
|
|
69
70
|
updated(el, binding) {
|
package/integrations/angular.js
CHANGED
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*
|
|
46
46
|
* @example Two-way binding with [(prop)]
|
|
47
|
-
* The
|
|
47
|
+
* The WccModel directive translates wcc:model events to Angular's propChange convention.
|
|
48
48
|
* Angular's banana-in-a-box [(prop)] expands to:
|
|
49
49
|
* [prop]="value" (propChange)="value = $event.detail"
|
|
50
50
|
*
|
|
51
51
|
* So when the WCC component emits wcc:model with { prop: 'value', value: 'new' },
|
|
52
|
-
* the
|
|
52
|
+
* the WccModel directive re-dispatches as 'valueChange' CustomEvent, which Angular picks up.
|
|
53
53
|
*
|
|
54
54
|
* @example ngModel support (requires ControlValueAccessor)
|
|
55
55
|
* For ngModel/ReactiveForms, see the WccValueAccessor guide in:
|
package/integrations/vue.js
CHANGED
|
@@ -65,11 +65,9 @@ export function wccVuePlugin(options = {}) {
|
|
|
65
65
|
|
|
66
66
|
let result = code
|
|
67
67
|
|
|
68
|
-
// NOTE:
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// Event object to the ref — it doesn't extract .detail automatically.
|
|
72
|
-
// This transform rewrites v-model:prop to @prop-changed="ref = $event.detail".
|
|
68
|
+
// NOTE: The compiled WCC component emits only `wcc:model` as its single
|
|
69
|
+
// canonical model change event. Framework-specific event formats are handled
|
|
70
|
+
// by each framework's adapter/plugin.
|
|
73
71
|
//
|
|
74
72
|
// This plugin is needed for:
|
|
75
73
|
// 1. v-model:propName (Vue can't unwrap CustomEvent.detail natively)
|
|
@@ -78,11 +76,11 @@ export function wccVuePlugin(options = {}) {
|
|
|
78
76
|
|
|
79
77
|
// Transform v-model:propName="expr" on custom elements (tags with hyphens)
|
|
80
78
|
// Also handles modifiers: v-model:propName.trim.number="expr"
|
|
81
|
-
// → :propName="expr" @
|
|
82
|
-
// with modifiers applied to the
|
|
83
|
-
// .trim →
|
|
84
|
-
// .number → Number(
|
|
85
|
-
// .lazy →
|
|
79
|
+
// → :propName="expr" @wcc:model="$event.detail.prop === 'propName' && (expr = value)"
|
|
80
|
+
// with modifiers applied to the extracted value:
|
|
81
|
+
// .trim → value.trim() (for string values)
|
|
82
|
+
// .number → Number(value)
|
|
83
|
+
// .lazy → no-op for custom elements
|
|
86
84
|
// Run in a loop to handle multiple v-model on the same element
|
|
87
85
|
let prev = ''
|
|
88
86
|
while (prev !== result) {
|
|
@@ -91,7 +89,7 @@ export function wccVuePlugin(options = {}) {
|
|
|
91
89
|
/(<[\w]+-[\w-]*(?:\s[^>]*?)?)\bv-model:(\w+)((?:\.\w+)*)="([^"]+)"/,
|
|
92
90
|
(match, prefix, prop, modifiersStr, expr) => {
|
|
93
91
|
const modifiers = modifiersStr ? modifiersStr.slice(1).split('.') : []
|
|
94
|
-
let value = '$event.detail'
|
|
92
|
+
let value = '$event.detail.value'
|
|
95
93
|
// Apply modifiers in order
|
|
96
94
|
for (const mod of modifiers) {
|
|
97
95
|
if (mod === 'trim') {
|
|
@@ -101,14 +99,14 @@ export function wccVuePlugin(options = {}) {
|
|
|
101
99
|
}
|
|
102
100
|
// .lazy is a no-op for custom elements (they already use change events)
|
|
103
101
|
}
|
|
104
|
-
return `${prefix}:${prop}="${expr}"
|
|
102
|
+
return `${prefix}:${prop}="${expr}" @wcc:model="$event.detail.prop === '${prop}' && (${expr} = ${value})"`
|
|
105
103
|
}
|
|
106
104
|
)
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
// Transform v-model="expr" (without argument) on custom elements
|
|
110
108
|
// Also handles modifiers: v-model.trim.lazy="expr"
|
|
111
|
-
// → :model-value="expr" @model
|
|
109
|
+
// → :model-value="expr" @wcc:model="$event.detail.prop === 'modelValue' && (expr = value)"
|
|
112
110
|
prev = ''
|
|
113
111
|
while (prev !== result) {
|
|
114
112
|
prev = result
|
|
@@ -116,7 +114,7 @@ export function wccVuePlugin(options = {}) {
|
|
|
116
114
|
/(<[\w]+-[\w-]*(?:\s[^>]*?)?)\bv-model((?:\.\w+)*)="([^"]+)"/,
|
|
117
115
|
(match, prefix, modifiersStr, expr) => {
|
|
118
116
|
const modifiers = modifiersStr ? modifiersStr.slice(1).split('.') : []
|
|
119
|
-
let value = '$event.detail'
|
|
117
|
+
let value = '$event.detail.value'
|
|
120
118
|
for (const mod of modifiers) {
|
|
121
119
|
if (mod === 'trim') {
|
|
122
120
|
value = `(typeof ${value} === 'string' ? (${value}).trim() : ${value})`
|
|
@@ -124,7 +122,7 @@ export function wccVuePlugin(options = {}) {
|
|
|
124
122
|
value = `Number(${value})`
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
|
-
return `${prefix}:model-value="${expr}" @model
|
|
125
|
+
return `${prefix}:model-value="${expr}" @wcc:model="$event.detail.prop === 'modelValue' && (${expr} = ${value})"`
|
|
128
126
|
}
|
|
129
127
|
)
|
|
130
128
|
}
|
package/lib/codegen.js
CHANGED
|
@@ -1773,43 +1773,23 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1773
1773
|
}
|
|
1774
1774
|
|
|
1775
1775
|
// _emit method (if emits declared)
|
|
1776
|
-
// Emits the event
|
|
1777
|
-
//
|
|
1778
|
-
// 2. camelCase — for Angular WccEvents directive
|
|
1779
|
-
// 3. lowercase — for React 19 (onEventName → addEventListener('eventname'))
|
|
1776
|
+
// Emits the original event name + lowercase-no-hyphens for React 19 compatibility.
|
|
1777
|
+
// React 19 maps `oncountchanged` → addEventListener('countchanged').
|
|
1780
1778
|
if (emits.length > 0) {
|
|
1781
1779
|
lines.push(' _emit(name, detail) {');
|
|
1782
1780
|
lines.push(' const evt = { detail, bubbles: true, composed: true };');
|
|
1783
1781
|
lines.push(' this.dispatchEvent(new CustomEvent(name, evt));');
|
|
1784
|
-
|
|
1785
|
-
lines.push(
|
|
1786
|
-
lines.push(" const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());");
|
|
1787
|
-
lines.push(' this.dispatchEvent(new CustomEvent(camel, evt));');
|
|
1788
|
-
// lowercase version for React 19
|
|
1789
|
-
lines.push(' this.dispatchEvent(new CustomEvent(camel.toLowerCase(), evt));');
|
|
1790
|
-
lines.push(' } else {');
|
|
1791
|
-
// If no hyphens, just emit lowercase (for React 19)
|
|
1792
|
-
lines.push(' const lower = name.toLowerCase();');
|
|
1793
|
-
lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
|
|
1794
|
-
lines.push(' }');
|
|
1782
|
+
lines.push(" const lower = name.replace(/-/g, '').toLowerCase();");
|
|
1783
|
+
lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
|
|
1795
1784
|
lines.push(' }');
|
|
1796
1785
|
lines.push('');
|
|
1797
1786
|
}
|
|
1798
1787
|
|
|
1799
1788
|
// _modelSet methods (one per defineModel prop — emits events on internal write)
|
|
1800
1789
|
// Emits:
|
|
1801
|
-
// 1. wcc:model —
|
|
1802
|
-
// 2.
|
|
1803
|
-
// 3. propNameChanged — camelCase for Angular WccEvents / direct binding
|
|
1804
|
-
// 4. propnamechanged — lowercase for React 19 (onPropnameChanged → 'propnamechanged')
|
|
1805
|
-
// 5. propNameChange — for Angular [(prop)] banana-box syntax
|
|
1806
|
-
//
|
|
1807
|
-
// NOTE: Vue v-model:prop requires the wccVuePlugin because Vue assigns the raw
|
|
1808
|
-
// Event object to the ref (not event.detail). The plugin transforms v-model:prop
|
|
1809
|
-
// to @prop-changed="ref = $event.detail" which correctly extracts the value.
|
|
1790
|
+
// 1. wcc:model — canonical event for vanilla JS, WCC-to-WCC, React adapter, Vue plugin
|
|
1791
|
+
// 2. propNameChange — for Angular [(prop)] banana-box syntax (zero-config)
|
|
1810
1792
|
for (const md of modelDefs) {
|
|
1811
|
-
const kebabName = camelToKebab(md.name);
|
|
1812
|
-
const camelChanged = `${md.name}Changed`;
|
|
1813
1793
|
lines.push(` _modelSet_${md.name}(newVal) {`);
|
|
1814
1794
|
lines.push(` const oldVal = this._m_${md.name}();`);
|
|
1815
1795
|
lines.push(` this._m_${md.name}(newVal);`);
|
|
@@ -1818,13 +1798,6 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1818
1798
|
lines.push(` bubbles: true,`);
|
|
1819
1799
|
lines.push(` composed: true`);
|
|
1820
1800
|
lines.push(` }));`);
|
|
1821
|
-
// Kebab-case: prop-name-changed (Vue plugin, addEventListener)
|
|
1822
|
-
lines.push(` this.dispatchEvent(new CustomEvent('${kebabName}-changed', { detail: newVal, bubbles: true }));`);
|
|
1823
|
-
// camelCase: propNameChanged (Angular, addEventListener)
|
|
1824
|
-
lines.push(` this.dispatchEvent(new CustomEvent('${camelChanged}', { detail: newVal, bubbles: true }));`);
|
|
1825
|
-
// lowercase: propnamechanged (React 19)
|
|
1826
|
-
lines.push(` this.dispatchEvent(new CustomEvent('${camelChanged.toLowerCase()}', { detail: newVal, bubbles: true }));`);
|
|
1827
|
-
// Angular banana-box: propNameChange
|
|
1828
1801
|
lines.push(` this.dispatchEvent(new CustomEvent('${md.name}Change', { detail: newVal, bubbles: true }));`);
|
|
1829
1802
|
lines.push(' }');
|
|
1830
1803
|
lines.push('');
|
package/package.json
CHANGED