@sprlab/wccompiler 0.7.3 → 0.8.1
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.js +104 -0
- package/adapters/vue.js +132 -0
- package/integrations/angular.js +35 -10
- package/integrations/react.js +65 -1
- package/integrations/vue.js +14 -0
- package/lib/codegen.js +160 -29
- package/lib/compiler-browser.js +23 -4
- package/lib/compiler.js +94 -1
- package/lib/parser-extractors.js +105 -1
- package/lib/tree-walker.js +33 -5
- package/lib/types.js +9 -0
- package/package.json +5 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Angular adapter for WCC defineModel — enables [(propName)] two-way binding.
|
|
3
|
+
*
|
|
4
|
+
* Import this ONCE in your Angular app's main.ts:
|
|
5
|
+
* import '@sprlab/wccompiler/adapters/angular'
|
|
6
|
+
*
|
|
7
|
+
* What it does:
|
|
8
|
+
* 1. Translates wcc:model events → propNameChange (enables [(prop)] syntax)
|
|
9
|
+
* 2. Uses queueMicrotask to defer event emission outside Angular's render cycle
|
|
10
|
+
* (prevents NG0600: "Writing to signals is not allowed while Angular renders")
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* <!-- In Angular template (with CUSTOM_ELEMENTS_SCHEMA) -->
|
|
14
|
+
* <wcc-input [(value)]="text"></wcc-input>
|
|
15
|
+
* <wcc-counter [(count)]="myCount"></wcc-counter>
|
|
16
|
+
*
|
|
17
|
+
* For ngModel support, you need a ControlValueAccessor.
|
|
18
|
+
* See the exported WccValueAccessor class below.
|
|
19
|
+
*
|
|
20
|
+
* @module @sprlab/wccompiler/adapters/angular
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ── Document-level adapter: wcc:model → propNameChange ──────────────
|
|
24
|
+
// Angular's [(prop)] syntax listens for `propChange` events.
|
|
25
|
+
// Uses queueMicrotask to defer the re-dispatch outside Angular's synchronous
|
|
26
|
+
// render cycle, preventing NG0600 errors when Angular is mid-render.
|
|
27
|
+
|
|
28
|
+
if (typeof document !== 'undefined') {
|
|
29
|
+
document.addEventListener('wcc:model', (e) => {
|
|
30
|
+
const { prop, value } = e.detail;
|
|
31
|
+
const target = e.target;
|
|
32
|
+
|
|
33
|
+
// Defer to next microtask to avoid NG0600
|
|
34
|
+
// (Angular doesn't allow signal writes during render)
|
|
35
|
+
queueMicrotask(() => {
|
|
36
|
+
target.dispatchEvent(new CustomEvent(`${prop}Change`, {
|
|
37
|
+
detail: value,
|
|
38
|
+
bubbles: true
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── ControlValueAccessor for ngModel/ReactiveForms ──────────────────
|
|
45
|
+
// Angular's ngModel requires a ControlValueAccessor to bridge form controls.
|
|
46
|
+
// Since this is a JS file (not TypeScript with decorators), we export the
|
|
47
|
+
// implementation as a guide. Users need to create a TypeScript directive.
|
|
48
|
+
//
|
|
49
|
+
// Copy this into your Angular project as a .ts file:
|
|
50
|
+
//
|
|
51
|
+
// ```ts
|
|
52
|
+
// import { Directive, ElementRef, forwardRef, HostListener } from '@angular/core';
|
|
53
|
+
// import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
54
|
+
//
|
|
55
|
+
// @Directive({
|
|
56
|
+
// selector: '[wccModel]',
|
|
57
|
+
// providers: [{
|
|
58
|
+
// provide: NG_VALUE_ACCESSOR,
|
|
59
|
+
// useExisting: forwardRef(() => WccValueAccessor),
|
|
60
|
+
// multi: true
|
|
61
|
+
// }]
|
|
62
|
+
// })
|
|
63
|
+
// export class WccValueAccessor implements ControlValueAccessor {
|
|
64
|
+
// private onChange: (value: any) => void = () => {};
|
|
65
|
+
// private onTouched: () => void = () => {};
|
|
66
|
+
//
|
|
67
|
+
// constructor(private el: ElementRef<HTMLElement>) {}
|
|
68
|
+
//
|
|
69
|
+
// writeValue(value: any): void {
|
|
70
|
+
// // Parent → Child: set attribute
|
|
71
|
+
// if (value != null) {
|
|
72
|
+
// this.el.nativeElement.setAttribute('value', String(value));
|
|
73
|
+
// } else {
|
|
74
|
+
// this.el.nativeElement.removeAttribute('value');
|
|
75
|
+
// }
|
|
76
|
+
// }
|
|
77
|
+
//
|
|
78
|
+
// registerOnChange(fn: (value: any) => void): void {
|
|
79
|
+
// this.onChange = fn;
|
|
80
|
+
// }
|
|
81
|
+
//
|
|
82
|
+
// registerOnTouched(fn: () => void): void {
|
|
83
|
+
// this.onTouched = fn;
|
|
84
|
+
// }
|
|
85
|
+
//
|
|
86
|
+
// @HostListener('wcc:model', ['$event'])
|
|
87
|
+
// onModelChange(event: CustomEvent): void {
|
|
88
|
+
// if (event.detail && event.detail.prop === 'value') {
|
|
89
|
+
// this.onChange(event.detail.value);
|
|
90
|
+
// }
|
|
91
|
+
// }
|
|
92
|
+
//
|
|
93
|
+
// @HostListener('blur')
|
|
94
|
+
// onBlur(): void {
|
|
95
|
+
// this.onTouched();
|
|
96
|
+
// }
|
|
97
|
+
// }
|
|
98
|
+
// ```
|
|
99
|
+
//
|
|
100
|
+
// Usage with ngModel:
|
|
101
|
+
// <wcc-input wccModel [(ngModel)]="text"></wcc-input>
|
|
102
|
+
//
|
|
103
|
+
// Usage with Reactive Forms:
|
|
104
|
+
// <wcc-input wccModel [formControl]="myControl"></wcc-input>
|
package/adapters/vue.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue adapter for WCC defineModel — enables v-model and multi-model binding.
|
|
3
|
+
*
|
|
4
|
+
* Usage (ONE line in main.js):
|
|
5
|
+
* import { createApp } from 'vue'
|
|
6
|
+
* import { wccVue } from '@sprlab/wccompiler/adapters/vue'
|
|
7
|
+
*
|
|
8
|
+
* const app = createApp(App)
|
|
9
|
+
* app.use(wccVue) // registers adapter + v-wcc-model directive globally
|
|
10
|
+
* app.mount('#app')
|
|
11
|
+
*
|
|
12
|
+
* What it does:
|
|
13
|
+
* 1. Registers document-level wcc:model → update:propName translation (enables v-model)
|
|
14
|
+
* 2. Registers v-wcc-model directive globally (enables multi-prop two-way binding)
|
|
15
|
+
*
|
|
16
|
+
* Template usage:
|
|
17
|
+
* <!-- Single model prop (Vue's native v-model) -->
|
|
18
|
+
* <!-- Component must declare: defineModel({ name: 'modelValue', default: '' }) -->
|
|
19
|
+
* <wcc-input v-model="text"></wcc-input>
|
|
20
|
+
*
|
|
21
|
+
* <!-- Multiple model props (v-wcc-model:propName) -->
|
|
22
|
+
* <wcc-form v-model="mainValue" v-wcc-model:count="countRef" v-wcc-model:title="titleRef"></wcc-form>
|
|
23
|
+
*
|
|
24
|
+
* @module @sprlab/wccompiler/adapters/vue
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
// ── Document-level adapter: wcc:model → update:propName ─────────────
|
|
28
|
+
// This enables Vue's native v-model on WCC custom elements.
|
|
29
|
+
|
|
30
|
+
if (typeof document !== 'undefined') {
|
|
31
|
+
document.addEventListener('wcc:model', (e) => {
|
|
32
|
+
const { prop, value } = e.detail;
|
|
33
|
+
e.target.dispatchEvent(new CustomEvent(`update:${prop}`, {
|
|
34
|
+
detail: value,
|
|
35
|
+
bubbles: true
|
|
36
|
+
}));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Vue directive: v-wcc-model ──────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 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
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* <wcc-counter v-wcc-model:count="myCount"></wcc-counter>
|
|
51
|
+
*/
|
|
52
|
+
export const vWccModel = {
|
|
53
|
+
mounted(el, binding) {
|
|
54
|
+
const propName = binding.arg;
|
|
55
|
+
if (!propName) {
|
|
56
|
+
console.warn('[v-wcc-model] Missing argument. Usage: v-wcc-model:propName="ref"');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Set initial value (parent → child)
|
|
61
|
+
if (binding.value != null) {
|
|
62
|
+
el.setAttribute(propName, String(binding.value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Listen for child → parent changes via the update:propName event
|
|
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
|
|
73
|
+
const wccHandler = (e) => {
|
|
74
|
+
if (e.detail && e.detail.prop === propName) {
|
|
75
|
+
// Trigger Vue reactivity by emitting on the component instance
|
|
76
|
+
// This works because Vue tracks directive bindings
|
|
77
|
+
el.dispatchEvent(new CustomEvent(`update:${propName}`, {
|
|
78
|
+
detail: e.detail.value,
|
|
79
|
+
bubbles: false
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
el.addEventListener('wcc:model', wccHandler);
|
|
85
|
+
// Store handler for cleanup
|
|
86
|
+
el.__wccModelHandlers = el.__wccModelHandlers || {};
|
|
87
|
+
el.__wccModelHandlers[propName] = wccHandler;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
updated(el, binding) {
|
|
91
|
+
const propName = binding.arg;
|
|
92
|
+
if (!propName) return;
|
|
93
|
+
|
|
94
|
+
// Sync parent → child on updates
|
|
95
|
+
if (binding.value != null) {
|
|
96
|
+
el.setAttribute(propName, String(binding.value));
|
|
97
|
+
} else {
|
|
98
|
+
el.removeAttribute(propName);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
beforeUnmount(el, binding) {
|
|
103
|
+
const propName = binding.arg;
|
|
104
|
+
if (!propName) return;
|
|
105
|
+
|
|
106
|
+
// Cleanup listener
|
|
107
|
+
const handler = el.__wccModelHandlers?.[propName];
|
|
108
|
+
if (handler) {
|
|
109
|
+
el.removeEventListener('wcc:model', handler);
|
|
110
|
+
delete el.__wccModelHandlers[propName];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// ── Vue Plugin: app.use(wccVue) ─────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Vue plugin that registers the wcc:model adapter and v-wcc-model directive globally.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* import { createApp } from 'vue'
|
|
122
|
+
* import { wccVue } from '@sprlab/wccompiler/adapters/vue'
|
|
123
|
+
*
|
|
124
|
+
* const app = createApp(App)
|
|
125
|
+
* app.use(wccVue)
|
|
126
|
+
* app.mount('#app')
|
|
127
|
+
*/
|
|
128
|
+
export const wccVue = {
|
|
129
|
+
install(app) {
|
|
130
|
+
app.directive('wcc-model', vWccModel);
|
|
131
|
+
}
|
|
132
|
+
};
|
package/integrations/angular.js
CHANGED
|
@@ -3,21 +3,34 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module @sprlab/wccompiler/integrations/angular
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Setup requires two steps:
|
|
7
|
+
*
|
|
8
|
+
* 1. Import the adapter in main.ts (enables [(prop)] two-way binding):
|
|
9
|
+
* ```ts
|
|
10
|
+
* import '@sprlab/wccompiler/adapters/angular'
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* 2. Add CUSTOM_ELEMENTS_SCHEMA to your component/module:
|
|
9
14
|
*
|
|
10
15
|
* @example Standalone component (Angular 17+)
|
|
11
16
|
* ```ts
|
|
12
|
-
* import { Component } from '@angular/core';
|
|
13
|
-
* import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
17
|
+
* import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
14
18
|
*
|
|
15
19
|
* @Component({
|
|
16
20
|
* selector: 'app-root',
|
|
17
21
|
* schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
18
|
-
* template:
|
|
22
|
+
* template: `
|
|
23
|
+
* <!-- Simple one-way binding -->
|
|
24
|
+
* <wcc-counter [count]="myCount"></wcc-counter>
|
|
25
|
+
*
|
|
26
|
+
* <!-- Two-way binding with [(prop)] -->
|
|
27
|
+
* <wcc-input [(value)]="text"></wcc-input>
|
|
28
|
+
* `
|
|
19
29
|
* })
|
|
20
|
-
* export class AppComponent {
|
|
30
|
+
* export class AppComponent {
|
|
31
|
+
* text = '';
|
|
32
|
+
* myCount = 0;
|
|
33
|
+
* }
|
|
21
34
|
* ```
|
|
22
35
|
*
|
|
23
36
|
* @example NgModule approach
|
|
@@ -30,8 +43,19 @@
|
|
|
30
43
|
* export class AppModule {}
|
|
31
44
|
* ```
|
|
32
45
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
46
|
+
* @example Two-way binding with [(prop)]
|
|
47
|
+
* The adapter translates wcc:model events to Angular's propChange convention.
|
|
48
|
+
* Angular's banana-in-a-box [(prop)] expands to:
|
|
49
|
+
* [prop]="value" (propChange)="value = $event.detail"
|
|
50
|
+
*
|
|
51
|
+
* So when the WCC component emits wcc:model with { prop: 'value', value: 'new' },
|
|
52
|
+
* the adapter re-dispatches as 'valueChange' CustomEvent, which Angular picks up.
|
|
53
|
+
*
|
|
54
|
+
* @example ngModel support (requires ControlValueAccessor)
|
|
55
|
+
* For ngModel/ReactiveForms, see the WccValueAccessor guide in:
|
|
56
|
+
* @sprlab/wccompiler/adapters/angular
|
|
57
|
+
*
|
|
58
|
+
* That file contains a copy-paste TypeScript directive implementation.
|
|
35
59
|
*/
|
|
36
60
|
|
|
37
61
|
/**
|
|
@@ -39,10 +63,11 @@
|
|
|
39
63
|
* This is a documentation-only export — Angular's AOT compiler requires
|
|
40
64
|
* CUSTOM_ELEMENTS_SCHEMA to be imported directly from @angular/core.
|
|
41
65
|
*
|
|
42
|
-
* @type {{ schema: string, standalone: string, ngModule: string }}
|
|
66
|
+
* @type {{ schema: string, standalone: string, ngModule: string, adapter: string }}
|
|
43
67
|
*/
|
|
44
68
|
export const WCC_ANGULAR_CONFIG = {
|
|
45
69
|
schema: "import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'",
|
|
46
70
|
standalone: "@Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })",
|
|
47
71
|
ngModule: "@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })",
|
|
72
|
+
adapter: "import '@sprlab/wccompiler/adapters/angular' // in main.ts",
|
|
48
73
|
}
|
package/integrations/react.js
CHANGED
|
@@ -13,9 +13,14 @@
|
|
|
13
13
|
* // Form 2: Let the hook create the ref
|
|
14
14
|
* const ref = useWccEvent('change', (e) => console.log(e.detail))
|
|
15
15
|
* <wcc-counter ref={ref}></wcc-counter>
|
|
16
|
+
*
|
|
17
|
+
* // Form 3: Two-way binding with defineModel
|
|
18
|
+
* const [value, setValue] = useState('')
|
|
19
|
+
* const ref = useWccModel('value', value, setValue)
|
|
20
|
+
* <wcc-input ref={ref}></wcc-input>
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import { useRef, useEffect } from 'react'
|
|
23
|
+
import { useRef, useEffect, useCallback } from 'react'
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Hook that attaches a CustomEvent listener to a DOM element via ref.
|
|
@@ -50,3 +55,62 @@ export function useWccEvent(refOrEventName, eventNameOrHandler, handler) {
|
|
|
50
55
|
// Only return ref if we created it (Form 2)
|
|
51
56
|
if (!isRefForm) return elementRef
|
|
52
57
|
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook for two-way binding with WCC defineModel props.
|
|
62
|
+
*
|
|
63
|
+
* Listens for `wcc:model` events on the element and calls the setter
|
|
64
|
+
* when the matching prop changes internally. Also syncs the React state
|
|
65
|
+
* to the element's attribute when the value changes externally.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} propName - The model prop name (e.g., 'value', 'count')
|
|
68
|
+
* @param {*} value - Current React state value
|
|
69
|
+
* @param {(newValue: *) => void} setValue - React state setter
|
|
70
|
+
* @param {import('react').RefObject<HTMLElement>} [existingRef] - Optional existing ref
|
|
71
|
+
* @returns {import('react').RefObject<HTMLElement>} Ref to attach to the WCC element
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```jsx
|
|
75
|
+
* function App() {
|
|
76
|
+
* const [text, setText] = useState('')
|
|
77
|
+
* const inputRef = useWccModel('value', text, setText)
|
|
78
|
+
* return <wcc-input ref={inputRef}></wcc-input>
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function useWccModel(propName, value, setValue, existingRef) {
|
|
83
|
+
const internalRef = useRef(null)
|
|
84
|
+
const elementRef = existingRef || internalRef
|
|
85
|
+
|
|
86
|
+
const setValueRef = useRef(setValue)
|
|
87
|
+
setValueRef.current = setValue
|
|
88
|
+
|
|
89
|
+
// Listen for wcc:model events from the component (child → parent)
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const el = elementRef.current
|
|
92
|
+
if (!el) return
|
|
93
|
+
|
|
94
|
+
const listener = (e) => {
|
|
95
|
+
if (e.detail && e.detail.prop === propName) {
|
|
96
|
+
setValueRef.current(e.detail.value)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
el.addEventListener('wcc:model', listener)
|
|
101
|
+
return () => el.removeEventListener('wcc:model', listener)
|
|
102
|
+
}, [propName])
|
|
103
|
+
|
|
104
|
+
// Sync React state to the element's attribute (parent → child)
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const el = elementRef.current
|
|
107
|
+
if (!el) return
|
|
108
|
+
if (value != null) {
|
|
109
|
+
el.setAttribute(propName, String(value))
|
|
110
|
+
} else {
|
|
111
|
+
el.removeAttribute(propName)
|
|
112
|
+
}
|
|
113
|
+
}, [propName, value])
|
|
114
|
+
|
|
115
|
+
return elementRef
|
|
116
|
+
}
|
package/integrations/vue.js
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
* Configures isCustomElement to recognize WCC component tags.
|
|
4
4
|
*
|
|
5
5
|
* @module @sprlab/wccompiler/integrations/vue
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: This file is for vite.config.js (Node.js context).
|
|
8
|
+
* For browser-side model adapter, import '@sprlab/wccompiler/adapters/vue' in your main.js.
|
|
9
|
+
*
|
|
10
|
+
* @example vite.config.js
|
|
11
|
+
* ```js
|
|
12
|
+
* import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
|
|
13
|
+
* export default { plugins: [wccVuePlugin()] }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example main.js (browser — enables v-model on WCC components)
|
|
17
|
+
* ```js
|
|
18
|
+
* import '@sprlab/wccompiler/adapters/vue'
|
|
19
|
+
* ```
|
|
6
20
|
*/
|
|
7
21
|
|
|
8
22
|
import vue from '@vitejs/plugin-vue'
|