@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.
@@ -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.dispatchEvent(new CustomEvent(`${prop}Change`, {
5
- detail: value,
6
- bubbles: true
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
+ };
@@ -3,21 +3,34 @@
3
3
  *
4
4
  * @module @sprlab/wccompiler/integrations/angular
5
5
  *
6
- * Angular's AOT compiler requires schemas to be statically analyzable,
7
- * so we cannot provide a re-exported schema constant that works at compile time.
8
- * Instead, use Angular's built-in CUSTOM_ELEMENTS_SCHEMA directly:
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: `<wcc-counter></wcc-counter>`
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 defineModel
34
- * ```ts
35
- * // The adapter translates wcc:model events to Angular's propNameChange convention.
36
- * // Import the integration once in your main.ts or app module:
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
- * // Then use Angular's banana-in-a-box syntax:
40
- * // <wcc-input [(value)]="myValue"></wcc-input>
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's it one line of config. WCC components work as native custom elements
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
  }
@@ -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 = this._${b.name}() ?? '';`);
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
- ref = `this._${pb.expr}()`;
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
- lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {