@sprlab/wccompiler 0.10.11 → 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/README.md +166 -65
- 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/README.md
CHANGED
|
@@ -695,38 +695,20 @@ Each standalone component has its own isolated reactive runtime. Signals from co
|
|
|
695
695
|
|
|
696
696
|
## Framework Integrations
|
|
697
697
|
|
|
698
|
-
WCC components are native custom elements — they work in any framework. Props, events, and named slots work natively with zero WCC-specific config. Two-way binding is zero-config in Angular; Vue
|
|
698
|
+
WCC components are native custom elements — they work in any framework. Props, events, and named slots work natively with zero WCC-specific config. Two-way binding is zero-config in Angular; Vue requires a plugin. Scoped slots require a framework plugin or directive for idiomatic syntax.
|
|
699
699
|
|
|
700
|
-
###
|
|
700
|
+
### Feature Support Matrix
|
|
701
701
|
|
|
702
|
-
|
|
702
|
+
| Feature | Vue (plugin) | Angular (directive) | React 19 (plugin) |
|
|
703
|
+
|---------|--------------|--------------------|--------------------|
|
|
704
|
+
| Props | ✅ `:count="ref"` | ✅ `[count]="signal()"` | ✅ `count={state}` |
|
|
705
|
+
| Events | ✅ `@count-changed="handler($event.detail)"` | ✅ `(count-changed)="handler($event.detail)"` | ✅ `oncountchanged={(e) => handler(e.detail)}` |
|
|
706
|
+
| Two-way binding | ✅ `v-model:count="ref"` | ✅ `[(count)]="signal"` | ❌ Not applicable |
|
|
707
|
+
| Default slot | ✅ children | ✅ children | ✅ children |
|
|
708
|
+
| Named slots | ✅ `<template #name>` | ✅ `<div slot-name>` | ✅ `<WccCard.Header>` |
|
|
709
|
+
| Scoped slots | ✅ `<template #name="{ prop }">` | ✅ `<ng-template slot="name" let-prop>` | ✅ `<WccList.Item>{(prop) => jsx}</WccList.Item>` |
|
|
703
710
|
|
|
704
|
-
|
|
705
|
-
<script type="module" src="dist/wcc-counter.js"></script>
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
**Vue:**
|
|
709
|
-
```vue
|
|
710
|
-
<wcc-counter :count="ref" @count-changed="handler($event.detail)">
|
|
711
|
-
<div slot="footer">Footer content</div>
|
|
712
|
-
</wcc-counter>
|
|
713
|
-
```
|
|
714
|
-
|
|
715
|
-
**React 19:**
|
|
716
|
-
```jsx
|
|
717
|
-
<wcc-counter count={state} onCountchanged={(e) => handler(e.detail)}>
|
|
718
|
-
<div slot="footer">Footer content</div>
|
|
719
|
-
</wcc-counter>
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
**Angular:**
|
|
723
|
-
```html
|
|
724
|
-
<wcc-counter [count]="signal()" (count-changed)="handler($event.detail)" [(count)]="signal">
|
|
725
|
-
<div slot="footer">Footer content</div>
|
|
726
|
-
</wcc-counter>
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
### Vue Plugin (v-model, modifiers, scoped slots)
|
|
711
|
+
### Vue (with `wccVuePlugin`)
|
|
730
712
|
|
|
731
713
|
```js
|
|
732
714
|
// vite.config.js
|
|
@@ -735,65 +717,184 @@ export default defineConfig({ plugins: [wccVuePlugin()] })
|
|
|
735
717
|
```
|
|
736
718
|
|
|
737
719
|
```vue
|
|
738
|
-
<
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
</
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
The plugin is needed for: v-model:prop (Vue assigns raw Event, not detail), v-model modifiers (.trim, .number), and scoped slot syntax (`{{prop}}` → `{%prop%}` escape).
|
|
746
|
-
|
|
747
|
-
Without the plugin, use `:prop` + `@prop-changed` for two-way binding manually.
|
|
748
|
-
|
|
749
|
-
### React Plugin + Wrappers (scoped slots, typed events, two-way)
|
|
750
|
-
|
|
751
|
-
```js
|
|
752
|
-
// vite.config.js
|
|
753
|
-
import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
|
|
754
|
-
import react from '@vitejs/plugin-react'
|
|
755
|
-
export default defineConfig({ plugins: [wccReactPlugin(), react()] })
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
```jsx
|
|
759
|
-
// Auto-generated wrappers — events unwrapped, React-idiomatic naming
|
|
760
|
-
import { createWccWrappers } from '@sprlab/wccompiler/adapters/react'
|
|
761
|
-
const { WccCounter, WccCard } = createWccWrappers()
|
|
720
|
+
<script setup>
|
|
721
|
+
import { ref } from 'vue'
|
|
722
|
+
const count = ref(0)
|
|
723
|
+
const text = ref('')
|
|
724
|
+
</script>
|
|
762
725
|
|
|
763
|
-
<
|
|
764
|
-
|
|
765
|
-
<
|
|
766
|
-
|
|
726
|
+
<template>
|
|
727
|
+
<!-- Props -->
|
|
728
|
+
<wcc-counter :count="count" label="Clicks"></wcc-counter>
|
|
729
|
+
|
|
730
|
+
<!-- Events -->
|
|
731
|
+
<wcc-counter @count-changed="count = $event.detail"></wcc-counter>
|
|
732
|
+
|
|
733
|
+
<!-- Two-way binding (v-model) -->
|
|
734
|
+
<wcc-counter v-model:count="count"></wcc-counter>
|
|
735
|
+
<wcc-input v-model.trim="text"></wcc-input>
|
|
736
|
+
|
|
737
|
+
<!-- Default slot -->
|
|
738
|
+
<wcc-card>
|
|
739
|
+
<p>Body content</p>
|
|
740
|
+
</wcc-card>
|
|
741
|
+
|
|
742
|
+
<!-- Named slots -->
|
|
743
|
+
<wcc-card>
|
|
744
|
+
<template #header><strong>Title</strong></template>
|
|
745
|
+
<p>Body</p>
|
|
746
|
+
<template #footer>Footer text</template>
|
|
747
|
+
</wcc-card>
|
|
748
|
+
|
|
749
|
+
<!-- Scoped slots -->
|
|
750
|
+
<wcc-list>
|
|
751
|
+
<template #item="{ item, index }">
|
|
752
|
+
<li>{{ index }}: {{ item }}</li>
|
|
753
|
+
</template>
|
|
754
|
+
</wcc-list>
|
|
755
|
+
</template>
|
|
767
756
|
```
|
|
768
757
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
### Angular Directive (scoped slots only)
|
|
758
|
+
The plugin provides: `isCustomElement` config, `v-model:prop` support, v-model modifiers (`.trim`, `.number`), and scoped slot syntax (`{{prop}}` → `{%prop%}` escape).
|
|
772
759
|
|
|
773
|
-
Angular
|
|
760
|
+
### Angular (with `WccSlotsDirective`)
|
|
774
761
|
|
|
775
762
|
```ts
|
|
763
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
776
764
|
import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular'
|
|
777
765
|
|
|
778
766
|
@Component({
|
|
779
767
|
imports: [WccSlotsDirective, WccSlotDef],
|
|
780
768
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
781
769
|
template: `
|
|
770
|
+
<!-- Props -->
|
|
771
|
+
<wcc-counter [count]="count" label="Clicks"></wcc-counter>
|
|
772
|
+
|
|
773
|
+
<!-- Events -->
|
|
774
|
+
<wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
775
|
+
|
|
776
|
+
<!-- Two-way binding (banana-box) -->
|
|
777
|
+
<wcc-counter [(count)]="count"></wcc-counter>
|
|
778
|
+
|
|
779
|
+
<!-- Default slot -->
|
|
780
|
+
<wcc-card>
|
|
781
|
+
<p>Body content</p>
|
|
782
|
+
</wcc-card>
|
|
783
|
+
|
|
784
|
+
<!-- Named slots -->
|
|
782
785
|
<wcc-card wccSlots>
|
|
783
|
-
<
|
|
784
|
-
<
|
|
786
|
+
<strong slot-header>Title</strong>
|
|
787
|
+
<p>Body</p>
|
|
788
|
+
<span slot-footer>Footer text</span>
|
|
785
789
|
</wcc-card>
|
|
790
|
+
|
|
791
|
+
<!-- Scoped slots -->
|
|
792
|
+
<wcc-list wccSlots>
|
|
793
|
+
<ng-template slot="item" let-item let-index="index">
|
|
794
|
+
<li>{{ index }}: {{ item }}</li>
|
|
795
|
+
</ng-template>
|
|
796
|
+
</wcc-list>
|
|
786
797
|
`
|
|
787
798
|
})
|
|
799
|
+
export class AppComponent {
|
|
800
|
+
count = 0
|
|
801
|
+
onCount(value: number) { this.count = value }
|
|
802
|
+
}
|
|
788
803
|
```
|
|
789
804
|
|
|
790
|
-
|
|
805
|
+
Angular needs no plugin for props, events, or two-way binding — only `CUSTOM_ELEMENTS_SCHEMA`. The directive is only needed for named slots (with `slot-name` syntax) and scoped slots.
|
|
806
|
+
|
|
807
|
+
### React 19 (with `wccReactPlugin`)
|
|
808
|
+
|
|
809
|
+
```js
|
|
810
|
+
// vite.config.js
|
|
811
|
+
import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
|
|
812
|
+
import react from '@vitejs/plugin-react'
|
|
813
|
+
export default defineConfig({ plugins: [wccReactPlugin({ prefix: 'wcc-' }), react()] })
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
```jsx
|
|
817
|
+
import { useState } from 'react'
|
|
818
|
+
import { WccCard, WccList } from './dist/wcc-react'
|
|
819
|
+
|
|
820
|
+
export default function App() {
|
|
821
|
+
const [count, setCount] = useState(0)
|
|
822
|
+
|
|
823
|
+
return (
|
|
824
|
+
<>
|
|
825
|
+
{/* Props */}
|
|
826
|
+
<wcc-counter count={count} label="Clicks"></wcc-counter>
|
|
827
|
+
|
|
828
|
+
{/* Events */}
|
|
829
|
+
<wcc-counter oncountchanged={(e) => setCount(e.detail)}></wcc-counter>
|
|
830
|
+
|
|
831
|
+
{/* Default slot */}
|
|
832
|
+
<WccCard>
|
|
833
|
+
<p>Body content</p>
|
|
834
|
+
</WccCard>
|
|
835
|
+
|
|
836
|
+
{/* Named slots (compound pattern) */}
|
|
837
|
+
<WccCard>
|
|
838
|
+
<WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
839
|
+
<p>Body</p>
|
|
840
|
+
<WccCard.Footer>Footer text</WccCard.Footer>
|
|
841
|
+
</WccCard>
|
|
842
|
+
|
|
843
|
+
{/* Named slots (props pattern) */}
|
|
844
|
+
<wcc-card header={<strong>Title</strong>} footer="Footer text">
|
|
845
|
+
<p>Body</p>
|
|
846
|
+
</wcc-card>
|
|
847
|
+
|
|
848
|
+
{/* Scoped slots (compound pattern) */}
|
|
849
|
+
<WccList>
|
|
850
|
+
<WccList.Item>{(item, index) => <li>{index}: {item}</li>}</WccList.Item>
|
|
851
|
+
</WccList>
|
|
852
|
+
|
|
853
|
+
{/* Scoped slots (render prop pattern) */}
|
|
854
|
+
<wcc-list renderItem={(item, index) => <li>{index}: {item}</li>} />
|
|
855
|
+
</>
|
|
856
|
+
)
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
The plugin transforms PascalCase tags, compound components, props-as-slots, and render props at build time. Import stubs from `./dist/wcc-react` (auto-generated by `wcc build`).
|
|
861
|
+
|
|
862
|
+
### Vanilla (no framework)
|
|
791
863
|
|
|
792
864
|
No configuration needed:
|
|
793
865
|
|
|
794
866
|
```html
|
|
795
867
|
<script type="module" src="dist/wcc-counter.js"></script>
|
|
796
|
-
<wcc-
|
|
868
|
+
<script type="module" src="dist/wcc-card.js"></script>
|
|
869
|
+
<script type="module" src="dist/wcc-list.js"></script>
|
|
870
|
+
|
|
871
|
+
<!-- Props (attributes) -->
|
|
872
|
+
<wcc-counter count="0" label="Clicks"></wcc-counter>
|
|
873
|
+
|
|
874
|
+
<!-- Events -->
|
|
875
|
+
<script>
|
|
876
|
+
document.querySelector('wcc-counter')
|
|
877
|
+
.addEventListener('count-changed', (e) => console.log(e.detail))
|
|
878
|
+
</script>
|
|
879
|
+
|
|
880
|
+
<!-- Default slot -->
|
|
881
|
+
<wcc-card>
|
|
882
|
+
<p>Body content</p>
|
|
883
|
+
</wcc-card>
|
|
884
|
+
|
|
885
|
+
<!-- Named slots -->
|
|
886
|
+
<wcc-card>
|
|
887
|
+
<strong slot="header">Title</strong>
|
|
888
|
+
<p>Body</p>
|
|
889
|
+
<span slot="footer">Footer text</span>
|
|
890
|
+
</wcc-card>
|
|
891
|
+
|
|
892
|
+
<!-- Scoped slots -->
|
|
893
|
+
<wcc-list>
|
|
894
|
+
<template #item="{ item, index }">
|
|
895
|
+
<li>{{index}}: {{item}}</li>
|
|
896
|
+
</template>
|
|
897
|
+
</wcc-list>
|
|
797
898
|
```
|
|
798
899
|
|
|
799
900
|
## Editor Support
|
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