@sprlab/wccompiler 0.8.4 → 0.8.7

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,51 +1,32 @@
1
1
  /**
2
- * Angular adapter for WCC defineModel — enables [(propName)] two-way binding.
2
+ * Angular adapter for WCC defineModel (OPTIONAL).
3
3
  *
4
- * Import this ONCE in your Angular app's main.ts:
5
- * import '@sprlab/wccompiler/adapters/angular'
4
+ * The WCC component already emits `propNameChange` directly from _modelSet,
5
+ * so Angular's [(prop)] banana-box syntax works WITHOUT this adapter.
6
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")
7
+ * This file is kept for:
8
+ * 1. Documentation of the Angular integration approach
9
+ * 2. The ControlValueAccessor guide for ngModel support
10
+ *
11
+ * Setup (Angular):
12
+ * // No adapter import needed! Just use CUSTOM_ELEMENTS_SCHEMA:
13
+ * import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
14
+ * @Component({ schemas: [CUSTOM_ELEMENTS_SCHEMA] })
11
15
  *
12
16
  * Usage:
13
- * <!-- In Angular template (with CUSTOM_ELEMENTS_SCHEMA) -->
14
17
  * <wcc-input [(value)]="text"></wcc-input>
15
18
  * <wcc-counter [(count)]="myCount"></wcc-counter>
16
19
  *
17
- * For ngModel support, you need a ControlValueAccessor.
18
- * See the exported WccValueAccessor class below.
20
+ * How it works:
21
+ * Angular's [(prop)] expands to [prop]="value" (propChange)="value = $event.detail"
22
+ * WCC _modelSet emits propNameChange CustomEvent with detail=newValue
23
+ * Angular picks it up automatically — no adapter needed.
19
24
  *
20
25
  * @module @sprlab/wccompiler/adapters/angular
21
26
  */
22
27
 
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
28
  // ── ControlValueAccessor for ngModel/ReactiveForms ──────────────────
45
29
  // 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
30
  // Copy this into your Angular project as a .ts file:
50
31
  //
51
32
  // ```ts
@@ -67,7 +48,6 @@ if (typeof document !== 'undefined') {
67
48
  // constructor(private el: ElementRef<HTMLElement>) {}
68
49
  //
69
50
  // writeValue(value: any): void {
70
- // // Parent → Child: set attribute
71
51
  // if (value != null) {
72
52
  // this.el.nativeElement.setAttribute('value', String(value));
73
53
  // } else {
@@ -96,9 +76,3 @@ if (typeof document !== 'undefined') {
96
76
  // }
97
77
  // }
98
78
  // ```
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,63 +1,30 @@
1
1
  /**
2
- * Vue adapter for WCC defineModel — enables v-model and multi-model binding.
2
+ * Vue adapter for WCC defineModel (OPTIONAL only needed without wccVuePlugin).
3
3
  *
4
- * Setup (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
- * IMPORTANT: Also use wccVuePlugin() in vite.config.js to enable v-model:propName
13
- * on custom elements (via AST nodeTransform):
14
- * import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
15
- * export default { plugins: [wccVuePlugin()] }
4
+ * If you use wccVuePlugin() in vite.config.js, you DON'T need this adapter.
5
+ * The plugin handles v-model:propName transformation at build time.
16
6
  *
17
- * With both configured, you can use:
18
- * <!-- Native v-model:propName (preferred, requires wccVuePlugin) -->
19
- * <wcc-input v-model:value="text"></wcc-input>
20
- * <wcc-form v-model:count="countRef" v-model:title="titleRef"></wcc-form>
7
+ * This adapter is for non-Vite setups (webpack, etc.) where you can't use
8
+ * the Vite pre-transform plugin. It provides a Vue directive for two-way binding.
21
9
  *
22
- * <!-- v-model without argument (uses modelValue convention) -->
23
- * <wcc-input v-model="text"></wcc-input>
10
+ * Setup:
11
+ * import { createApp } from 'vue'
12
+ * import { wccVue } from '@sprlab/wccompiler/adapters/vue'
13
+ * app.use(wccVue)
24
14
  *
25
- * <!-- Fallback directive (for non-Vite setups without nodeTransform) -->
15
+ * Usage:
26
16
  * <wcc-input v-wcc-model:value="textRef"></wcc-input>
17
+ * <wcc-form v-wcc-model:count="countRef"></wcc-form>
27
18
  *
28
19
  * @module @sprlab/wccompiler/adapters/vue
29
20
  */
30
21
 
31
- // ── Document-level adapter: wcc:model → update:propName ─────────────
32
- // This enables Vue's native v-model on WCC custom elements.
33
- // Vue v-model on custom elements listens for `update:modelValue` by default.
34
- // The nodeTransform in wccVuePlugin makes v-model:propName listen for `update:propName`.
35
-
36
- if (typeof document !== 'undefined') {
37
- document.addEventListener('wcc:model', (e) => {
38
- const { prop, value } = e.detail;
39
- e.target.dispatchEvent(new CustomEvent(`update:${prop}`, {
40
- detail: value,
41
- bubbles: true
42
- }));
43
- });
44
- }
45
-
46
22
  // ── Vue directive: v-wcc-model ──────────────────────────────────────
47
- // Fallback for non-Vite setups. If using wccVuePlugin(), prefer v-model:propName instead.
48
- //
49
- // Usage:
50
- // <wcc-input v-wcc-model:value="textRef"></wcc-input>
51
- //
52
- // The bound value MUST be a Vue ref (or reactive property).
53
- // The directive writes directly to ref.value for WCC→Vue updates.
23
+ // Fallback for non-Vite setups. Listens for propName-changed events directly.
54
24
 
55
25
  /**
56
26
  * Vue custom directive for two-way binding with WCC defineModel props.
57
- * This is a FALLBACK prefer v-model:propName with wccVuePlugin() nodeTransform.
58
- *
59
- * @example
60
- * <wcc-counter v-wcc-model:count="myCountRef"></wcc-counter>
27
+ * Listens for `propName-changed` CustomEvent (emitted by WCC _modelSet).
61
28
  */
62
29
  export const vWccModel = {
63
30
  mounted(el, binding) {
@@ -68,62 +35,41 @@ export const vWccModel = {
68
35
  }
69
36
 
70
37
  // Set initial value (parent → child)
71
- // Vue sets camelCase attributes, so set both camelCase and kebab-case
72
38
  if (binding.value != null) {
73
39
  el.setAttribute(propName, String(binding.value));
74
40
  }
75
41
 
76
- // Listen for childparent changes
77
- const wccHandler = (e) => {
78
- if (e.detail && e.detail.prop === propName) {
79
- const newValue = e.detail.value;
80
-
81
- // Try to update the Vue ref directly
82
- // In Vue 3, if the binding expression is a ref, binding.value is the ref's current value
83
- // We need to find the ref on the component instance and write to it
84
- const instance = binding.instance;
85
- if (instance) {
86
- // Access the setup state to find the ref
87
- const setupState = instance.$.setupState;
88
- // The binding expression is stored in the directive's internal data
89
- // In Vue 3, we can use the dir's exp to find the variable name
90
- // Fallback: emit a custom event that a parent @update handler can catch
91
- const refName = binding.dir?.__wccRefName?.[el]?.[propName];
92
- if (refName && setupState[refName] !== undefined) {
93
- // Direct ref write
94
- if (setupState[refName]?.value !== undefined) {
95
- setupState[refName].value = newValue;
42
+ // Listen for propName-changed (WCC Vue)
43
+ const kebabName = propName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
44
+ const handler = (e) => {
45
+ // Try to update the Vue ref via setupState
46
+ const instance = binding.instance;
47
+ if (instance) {
48
+ const setupState = instance.$.setupState;
49
+ // Find the ref that matches the current binding value
50
+ for (const key of Object.keys(setupState)) {
51
+ const val = setupState[key];
52
+ if (val === binding.value || val?.value === binding.value) {
53
+ if (val?.value !== undefined) {
54
+ val.value = e.detail;
96
55
  } else {
97
- setupState[refName] = newValue;
98
- }
99
- } else {
100
- // Fallback: try to find by matching current value
101
- for (const key of Object.keys(setupState)) {
102
- const val = setupState[key];
103
- if (val?.value === binding.value || val === binding.value) {
104
- if (val?.value !== undefined) {
105
- val.value = newValue;
106
- } else {
107
- setupState[key] = newValue;
108
- }
109
- break;
110
- }
56
+ setupState[key] = e.detail;
111
57
  }
58
+ break;
112
59
  }
113
60
  }
114
61
  }
115
62
  };
116
63
 
117
- el.addEventListener('wcc:model', wccHandler);
64
+ el.addEventListener(`${kebabName}-changed`, handler);
118
65
  el.__wccModelHandlers = el.__wccModelHandlers || {};
119
- el.__wccModelHandlers[propName] = wccHandler;
66
+ el.__wccModelHandlers[propName] = { handler, eventName: `${kebabName}-changed` };
120
67
  },
121
68
 
122
69
  updated(el, binding) {
123
70
  const propName = binding.arg;
124
71
  if (!propName) return;
125
72
 
126
- // Sync parent → child on updates
127
73
  if (binding.value != null) {
128
74
  el.setAttribute(propName, String(binding.value));
129
75
  } else {
@@ -135,27 +81,19 @@ export const vWccModel = {
135
81
  const propName = binding.arg;
136
82
  if (!propName) return;
137
83
 
138
- const handler = el.__wccModelHandlers?.[propName];
139
- if (handler) {
140
- el.removeEventListener('wcc:model', handler);
84
+ const entry = el.__wccModelHandlers?.[propName];
85
+ if (entry) {
86
+ el.removeEventListener(entry.eventName, entry.handler);
141
87
  delete el.__wccModelHandlers[propName];
142
88
  }
143
89
  }
144
90
  };
145
91
 
146
- // ── Vue Plugin: app.use(wccVue) ─────────────────────────────────────
92
+ // ── Vue Plugin ──────────────────────────────────────────────────────
147
93
 
148
94
  /**
149
- * Vue plugin that registers the v-wcc-model directive globally.
150
- * The document-level adapter is registered on import (side-effect above).
151
- *
152
- * @example
153
- * import { createApp } from 'vue'
154
- * import { wccVue } from '@sprlab/wccompiler/adapters/vue'
155
- *
156
- * const app = createApp(App)
157
- * app.use(wccVue)
158
- * app.mount('#app')
95
+ * Vue plugin that registers v-wcc-model directive globally.
96
+ * Only needed if NOT using wccVuePlugin() in vite.config.js.
159
97
  */
160
98
  export const wccVue = {
161
99
  install(app) {
@@ -5,7 +5,7 @@
5
5
  * @module @sprlab/wccompiler/integrations/vue
6
6
  *
7
7
  * IMPORTANT: This file is for vite.config.js (Node.js context).
8
- * For browser-side model adapter, use app.use(wccVue) from '@sprlab/wccompiler/adapters/vue'.
8
+ * For browser-side, use app.use(wccVue) from '@sprlab/wccompiler/adapters/vue'.
9
9
  *
10
10
  * @example vite.config.js
11
11
  * ```js
@@ -13,17 +13,25 @@
13
13
  * export default { plugins: [wccVuePlugin()] }
14
14
  * ```
15
15
  *
16
- * @example main.js (browserenables v-model event translation)
16
+ * @example main.js (optionalonly needed if NOT using wccVuePlugin)
17
17
  * ```js
18
18
  * import { wccVue } from '@sprlab/wccompiler/adapters/vue'
19
19
  * app.use(wccVue)
20
20
  * ```
21
21
  *
22
- * With this plugin, v-model:propName works natively on WCC custom elements:
22
+ * With wccVuePlugin(), v-model:propName works natively on WCC custom elements:
23
23
  * ```vue
24
24
  * <wcc-input v-model="text"></wcc-input>
25
25
  * <wcc-form v-model:count="countRef" v-model:title="titleRef"></wcc-form>
26
26
  * ```
27
+ *
28
+ * How it works:
29
+ * The plugin runs BEFORE @vitejs/plugin-vue and rewrites the template string:
30
+ * v-model:count="expr" → :count="expr" @count-changed="expr = $event.detail"
31
+ * v-model="expr" → :model-value="expr" @model-value-changed="expr = $event.detail"
32
+ *
33
+ * The WCC component emits `propName-changed` CustomEvent with detail=value on internal writes.
34
+ * Vue compiles @propName-changed as a normal event listener (not filtered like update:*).
27
35
  */
28
36
 
29
37
  import vue from '@vitejs/plugin-vue'
@@ -34,115 +42,66 @@ import vue from '@vitejs/plugin-vue'
34
42
  */
35
43
 
36
44
  /**
37
- * AST node transform that enables v-model:propName on custom elements.
38
- *
39
- * Vue's compiler normally doesn't support v-model with arguments on custom elements.
40
- * This transform intercepts v-model:arg directives on custom elements and rewrites them
41
- * to the equivalent :prop + @update:prop binding that Vue understands.
45
+ * Vite plugin that pre-transforms v-model:propName on custom elements
46
+ * before Vue's compiler processes the template.
42
47
  *
43
- * Transforms:
44
- * <wcc-input v-model:value="text" />
45
- * Into the equivalent of:
46
- * <wcc-input :value="text" @update:value="text = $event" />
48
+ * This is necessary because Vue's compiler filters out `onUpdate:*` event listeners
49
+ * for custom elements (isModelListener check in patchProp). By rewriting to
50
+ * `@propName-changed`, we use an event name that Vue registers normally.
47
51
  *
48
- * @param {object} node - Vue compiler AST node
49
- * @param {object} context - Vue compiler transform context
52
+ * @param {WccVuePluginOptions} [options]
53
+ * @returns {import('vite').Plugin[]}
50
54
  */
51
- function wccVModelTransform(node, context) {
52
- // Only process element nodes (type 1 = ELEMENT)
53
- if (node.type !== 1) return;
54
-
55
- // Only process custom elements (tag contains a hyphen)
56
- if (!node.tag.includes('-')) return;
55
+ export function wccVuePlugin(options = {}) {
56
+ const prefix = typeof options.prefix === 'string' ? options.prefix : 'wcc-'
57
57
 
58
- // Find v-model directives with arguments
59
- const newProps = [];
60
- let modified = false;
58
+ const preTransformPlugin = {
59
+ name: 'vite-plugin-wcc-vmodel',
60
+ enforce: 'pre',
61
+ transform(code, id) {
62
+ if (!id.endsWith('.vue')) return null
61
63
 
62
- for (const prop of node.props) {
63
- // Check if this is a v-model directive (with or without argument)
64
- if (
65
- prop.type === 7 && // DIRECTIVE
66
- prop.name === 'model'
67
- ) {
68
- // Determine prop name: explicit arg or default 'modelValue'
69
- const propName = prop.arg ? prop.arg.content : 'modelValue';
70
- const expr = prop.exp;
64
+ let result = code
71
65
 
72
- if (!expr) {
73
- newProps.push(prop);
74
- continue;
66
+ // Transform v-model:propName="expr" on custom elements (tags with hyphens)
67
+ // → :propName="expr" @propName-changed="expr = $event.detail"
68
+ // Run in a loop to handle multiple v-model on the same element
69
+ let prev = ''
70
+ while (prev !== result) {
71
+ prev = result
72
+ result = result.replace(
73
+ /(<[\w]+-[\w-]*(?:\s[^>]*?)?)\bv-model:(\w+)="([^"]+)"/,
74
+ (match, prefix, prop, expr) => {
75
+ return `${prefix}:${prop}="${expr}" @${prop}-changed="${expr} = $event.detail"`
76
+ }
77
+ )
75
78
  }
76
79
 
77
- // Create the arg node (use existing or create for modelValue)
78
- const argNode = prop.arg || {
79
- type: 4, // SIMPLE_EXPRESSION
80
- content: 'modelValue',
81
- isStatic: true,
82
- constType: 3,
83
- loc: prop.loc
84
- };
85
-
86
- // Replace v-model:propName="expr" with:
87
- // :propName="expr" (bind directive)
88
- newProps.push({
89
- type: 7, // DIRECTIVE
90
- name: 'bind',
91
- arg: argNode,
92
- exp: expr,
93
- modifiers: [],
94
- loc: prop.loc
95
- });
96
-
97
- // @update:propName="$event => { expr = $event }" (on directive)
98
- newProps.push({
99
- type: 7, // DIRECTIVE
100
- name: 'on',
101
- arg: {
102
- type: 4, // SIMPLE_EXPRESSION
103
- content: `update:${propName}`,
104
- isStatic: true,
105
- constType: 3,
106
- loc: prop.loc
107
- },
108
- exp: {
109
- type: 4, // SIMPLE_EXPRESSION
110
- content: `$event => { ${expr.content} = $event.detail ?? $event }`,
111
- isStatic: false,
112
- constType: 0,
113
- loc: prop.loc
114
- },
115
- modifiers: [],
116
- loc: prop.loc
117
- });
80
+ // Transform v-model="expr" (without argument) on custom elements
81
+ // :model-value="expr" @model-value-changed="expr = $event.detail"
82
+ prev = ''
83
+ while (prev !== result) {
84
+ prev = result
85
+ result = result.replace(
86
+ /(<[\w]+-[\w-]*(?:\s[^>]*?)?)\bv-model="([^"]+)"/,
87
+ (match, prefix, expr) => {
88
+ return `${prefix}:model-value="${expr}" @model-value-changed="${expr} = $event.detail"`
89
+ }
90
+ )
91
+ }
118
92
 
119
- modified = true;
120
- } else {
121
- newProps.push(prop);
93
+ if (result !== code) return result
94
+ return null
122
95
  }
123
96
  }
124
97
 
125
- if (modified) {
126
- node.props = newProps;
127
- }
128
- }
129
-
130
- /**
131
- * Creates a Vite plugin that configures Vue's template compiler
132
- * to recognize custom elements with the given prefix and enables
133
- * v-model:propName on those elements.
134
- *
135
- * @param {WccVuePluginOptions} [options]
136
- * @returns {import('vite').Plugin}
137
- */
138
- export function wccVuePlugin(options = {}) {
139
- const prefix = typeof options.prefix === 'string' ? options.prefix : 'wcc-'
140
- return vue({
98
+ const vuePlugin = vue({
141
99
  template: {
142
100
  compilerOptions: {
143
- isCustomElement: (tag) => tag.startsWith(prefix),
144
- nodeTransforms: [wccVModelTransform]
101
+ isCustomElement: (tag) => tag.startsWith(prefix)
145
102
  }
146
103
  }
147
104
  })
105
+
106
+ return [preTransformPlugin, vuePlugin]
148
107
  }
package/lib/codegen.js CHANGED
@@ -1664,8 +1664,13 @@ export function generateComponent(parseResult, options = {}) {
1664
1664
  lines.push('');
1665
1665
  }
1666
1666
 
1667
- // _modelSet methods (one per defineModel prop — emits wcc:model on internal write)
1667
+ // _modelSet methods (one per defineModel prop — emits events on internal write)
1668
+ // Emits:
1669
+ // 1. wcc:model — generic event for vanilla JS and WCC-to-WCC binding
1670
+ // 2. propName-changed — for Vue v-model (Vue doesn't filter this name)
1671
+ // 3. propNameChange — for Angular [(prop)] banana-box syntax
1668
1672
  for (const md of modelDefs) {
1673
+ const kebabName = camelToKebab(md.name);
1669
1674
  lines.push(` _modelSet_${md.name}(newVal) {`);
1670
1675
  lines.push(` const oldVal = this._m_${md.name}();`);
1671
1676
  lines.push(` this._m_${md.name}(newVal);`);
@@ -1674,6 +1679,10 @@ export function generateComponent(parseResult, options = {}) {
1674
1679
  lines.push(` bubbles: true,`);
1675
1680
  lines.push(` composed: true`);
1676
1681
  lines.push(` }));`);
1682
+ // Vue: propName-changed (not filtered by Vue's isModelListener)
1683
+ lines.push(` this.dispatchEvent(new CustomEvent('${kebabName}-changed', { detail: newVal, bubbles: true }));`);
1684
+ // Angular: propNameChange (Angular's [(prop)] listens for propChange)
1685
+ lines.push(` this.dispatchEvent(new CustomEvent('${md.name}Change', { detail: newVal, bubbles: true }));`);
1677
1686
  lines.push(' }');
1678
1687
  lines.push('');
1679
1688
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.8.4",
3
+ "version": "0.8.7",
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": {