@sprlab/wccompiler 0.8.0 → 0.8.2
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 +99 -4
- package/adapters/vue.js +123 -0
- package/integrations/angular.js +33 -22
- package/integrations/vue.js +14 -5
- package/lib/codegen.js +9 -3
- package/package.json +1 -1
package/adapters/angular.js
CHANGED
|
@@ -1,9 +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
|
+
|
|
1
28
|
if (typeof document !== 'undefined') {
|
|
2
29
|
document.addEventListener('wcc:model', (e) => {
|
|
3
30
|
const { prop, value } = e.detail;
|
|
4
|
-
e.target
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
});
|
|
8
41
|
});
|
|
9
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
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
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
|
+
|
|
1
30
|
if (typeof document !== 'undefined') {
|
|
2
31
|
document.addEventListener('wcc:model', (e) => {
|
|
3
32
|
const { prop, value } = e.detail;
|
|
@@ -7,3 +36,97 @@ if (typeof document !== 'undefined') {
|
|
|
7
36
|
}));
|
|
8
37
|
});
|
|
9
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,33 +43,31 @@
|
|
|
30
43
|
* export class AppModule {}
|
|
31
44
|
* ```
|
|
32
45
|
*
|
|
33
|
-
* @example Two-way binding with
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* import '@sprlab/wccompiler/integrations/angular'
|
|
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"
|
|
38
50
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
|
42
57
|
*
|
|
43
|
-
* That
|
|
44
|
-
* in Angular without any additional wrapper or helper.
|
|
58
|
+
* That file contains a copy-paste TypeScript directive implementation.
|
|
45
59
|
*/
|
|
46
60
|
|
|
47
|
-
// Side-effect: registers document-level wcc:model → propNameChange translation
|
|
48
|
-
// This enables [(propName)] two-way binding on WCC components in Angular templates.
|
|
49
|
-
import '../adapters/angular.js'
|
|
50
|
-
|
|
51
61
|
/**
|
|
52
62
|
* Configuration instructions for Angular projects using WCC components.
|
|
53
63
|
* This is a documentation-only export — Angular's AOT compiler requires
|
|
54
64
|
* CUSTOM_ELEMENTS_SCHEMA to be imported directly from @angular/core.
|
|
55
65
|
*
|
|
56
|
-
* @type {{ schema: string, standalone: string, ngModule: string }}
|
|
66
|
+
* @type {{ schema: string, standalone: string, ngModule: string, adapter: string }}
|
|
57
67
|
*/
|
|
58
68
|
export const WCC_ANGULAR_CONFIG = {
|
|
59
69
|
schema: "import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'",
|
|
60
70
|
standalone: "@Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })",
|
|
61
71
|
ngModule: "@NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })",
|
|
72
|
+
adapter: "import '@sprlab/wccompiler/adapters/angular' // in main.ts",
|
|
62
73
|
}
|
package/integrations/vue.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Vue Vite plugin for WCC custom elements.
|
|
3
3
|
* Configures isCustomElement to recognize WCC component tags.
|
|
4
|
-
* Also re-exports the defineModel adapter for v-model support.
|
|
5
4
|
*
|
|
6
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
|
+
* ```
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
22
|
import vue from '@vitejs/plugin-vue'
|
|
10
23
|
|
|
11
|
-
// Side-effect: registers document-level wcc:model → update:propName translation
|
|
12
|
-
// This enables v-model:propName on WCC components in Vue templates.
|
|
13
|
-
import '../adapters/vue.js'
|
|
14
|
-
|
|
15
24
|
/**
|
|
16
25
|
* @typedef {Object} WccVuePluginOptions
|
|
17
26
|
* @property {string} [prefix='wcc-'] - Tag prefix for custom element detection
|
package/lib/codegen.js
CHANGED
|
@@ -1149,8 +1149,11 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1149
1149
|
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
1150
1150
|
lines.push(' }));');
|
|
1151
1151
|
} else if (b.type === 'signal') {
|
|
1152
|
+
// Check if this is a model var (needs _m_ prefix instead of _)
|
|
1153
|
+
const modelPropName = modelVarMap.get(b.name);
|
|
1154
|
+
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1152
1155
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1153
|
-
lines.push(` this.${b.varName}.textContent =
|
|
1156
|
+
lines.push(` this.${b.varName}.textContent = ${signalRef} ?? '';`);
|
|
1154
1157
|
lines.push(' }));');
|
|
1155
1158
|
} else if (b.type === 'computed') {
|
|
1156
1159
|
lines.push(' this.__disposers.push(__effect(() => {');
|
|
@@ -1201,7 +1204,8 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1201
1204
|
} else if (pb.type === 'computed') {
|
|
1202
1205
|
ref = `this._c_${pb.expr}()`;
|
|
1203
1206
|
} else if (pb.type === 'signal') {
|
|
1204
|
-
|
|
1207
|
+
const modelPropName = modelVarMap.get(pb.expr);
|
|
1208
|
+
ref = modelPropName ? `this._m_${modelPropName}()` : `this._${pb.expr}()`;
|
|
1205
1209
|
} else if (pb.type === 'constant') {
|
|
1206
1210
|
ref = `this._const_${pb.expr}`;
|
|
1207
1211
|
} else {
|
|
@@ -1725,7 +1729,9 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1725
1729
|
if (b.type === 'prop') {
|
|
1726
1730
|
lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
|
|
1727
1731
|
} else if (b.type === 'signal') {
|
|
1728
|
-
|
|
1732
|
+
const modelPropName = modelVarMap.get(b.name);
|
|
1733
|
+
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1734
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = ${signalRef} ?? ''; });`);
|
|
1729
1735
|
} else if (b.type === 'computed') {
|
|
1730
1736
|
lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
|
|
1731
1737
|
} else {
|
package/package.json
CHANGED