@sprlab/wccompiler 0.10.5 → 0.10.6

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 CHANGED
@@ -192,6 +192,11 @@ watch(count, (newVal, oldVal) => {
192
192
  watch(() => props.count, (newVal, oldVal) => {
193
193
  console.log(`Prop changed: ${oldVal} → ${newVal}`)
194
194
  })
195
+
196
+ // Watch a derived expression
197
+ watch(() => count() * 2, (newVal, oldVal) => {
198
+ console.log(`Doubled changed: ${oldVal} → ${newVal}`)
199
+ })
195
200
  ```
196
201
 
197
202
  `watch` observes a specific signal or getter and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
@@ -248,6 +253,58 @@ const emit = defineEmits<{ (e: 'change', value: number): void }>()
248
253
 
249
254
  The compiler validates emit calls against declared events at compile time.
250
255
 
256
+ ## defineModel (Two-Way Binding)
257
+
258
+ `defineModel` declares a prop that supports two-way binding across frameworks:
259
+
260
+ ```js
261
+ import { defineModel } from 'wcc'
262
+
263
+ const count = defineModel({ name: 'count', default: 0 })
264
+ const title = defineModel({ name: 'title', default: 'untitled' })
265
+ ```
266
+
267
+ Read and write like a signal:
268
+ ```js
269
+ count() // read current value
270
+ count.set(5) // write — updates internal state + emits events
271
+ ```
272
+
273
+ **Events emitted on write:**
274
+
275
+ | Event | Purpose |
276
+ |-------|---------|
277
+ | `count-changed` | Kebab-case — for Vue plugin, addEventListener |
278
+ | `countChanged` | camelCase — for Angular, direct binding |
279
+ | `countChange` | Angular `[(count)]` banana-box syntax |
280
+ | `wcc:model` | Generic — for vanilla JS and WCC-to-WCC |
281
+
282
+ **Usage per framework:**
283
+
284
+ ```vue
285
+ <!-- Vue (with plugin) -->
286
+ <wcc-counter v-model:count="ref"></wcc-counter>
287
+
288
+ <!-- Vue (without plugin) -->
289
+ <wcc-counter :count="ref" @count-changed="ref = $event.detail"></wcc-counter>
290
+ ```
291
+
292
+ ```jsx
293
+ // React (with wrapper)
294
+ <WccCounter count={count} onCountChange={(value) => setCount(value)} />
295
+
296
+ // React (native CE)
297
+ <wcc-counter count={count} oncountchanged={(e) => setCount(e.detail)} />
298
+ ```
299
+
300
+ ```html
301
+ <!-- Angular (zero-config two-way) -->
302
+ <wcc-counter [(count)]="signal"></wcc-counter>
303
+
304
+ <!-- Angular (manual) -->
305
+ <wcc-counter [count]="signal()" (countChange)="signal.set($event.detail)"></wcc-counter>
306
+ ```
307
+
251
308
  ## Template Directives
252
309
 
253
310
  ### Text Interpolation
@@ -638,63 +695,96 @@ Each standalone component has its own isolated reactive runtime. Signals from co
638
695
 
639
696
  ## Framework Integrations
640
697
 
641
- WCC components are native custom elements — they work in any framework. Optional integration helpers reduce configuration friction:
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 and React require a lightweight plugin. Scoped slots require a framework plugin for idiomatic syntax.
642
699
 
643
- ### Vue 3 (Vite)
700
+ ### Zero-Config (works immediately)
701
+
702
+ Just import the compiled component and use it:
703
+
704
+ ```html
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)
644
730
 
645
731
  ```js
646
732
  // vite.config.js
647
733
  import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
734
+ export default defineConfig({ plugins: [wccVuePlugin()] })
735
+ ```
648
736
 
649
- export default defineConfig({
650
- plugins: [wccVuePlugin()]
651
- })
652
-
653
- // Custom prefix:
654
- // plugins: [wccVuePlugin({ prefix: 'my-' })]
737
+ ```vue
738
+ <wcc-input v-model="text" v-model:count.number="count"></wcc-input>
739
+ <wcc-card>
740
+ <template #header><strong>Title</strong></template>
741
+ <template #stats="{ likes }">{{ likes }} likes</template>
742
+ </wcc-card>
655
743
  ```
656
744
 
657
- ### React
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)
658
750
 
659
- React 19+ supports custom elements natively. For React 18, use the event hook to bridge CustomEvents:
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
+ ```
660
757
 
661
758
  ```jsx
662
- import { useRef } from 'react'
663
- import { useWccEvent } from '@sprlab/wccompiler/integrations/react'
664
-
665
- function App() {
666
- // Form 1: Pass an existing ref
667
- const counterRef = useRef(null)
668
- useWccEvent(counterRef, 'change', (e) => console.log(e.detail))
669
- return <wcc-counter ref={counterRef}></wcc-counter>
670
- }
759
+ // Auto-generated wrappers events unwrapped, React-idiomatic naming
760
+ import { createWccWrappers } from '@sprlab/wccompiler/adapters/react'
761
+ const { WccCounter, WccCard } = createWccWrappers()
671
762
 
672
- // Form 2: Let the hook create the ref
673
- function App2() {
674
- const ref = useWccEvent('change', (e) => console.log(e.detail))
675
- return <wcc-counter ref={ref}></wcc-counter>
676
- }
763
+ <WccCounter count={count} onCountChange={(value) => setCount(value)} />
764
+ <WccCard renderStats={(likes) => <span>{likes} likes</span>}>
765
+ <p>Body</p>
766
+ </WccCard>
677
767
  ```
678
768
 
679
- ### Angular
769
+ Wrappers read component metadata (`static __meta`) automatically — no manual event/model config needed.
770
+
771
+ ### Angular Directive (scoped slots only)
680
772
 
681
- Add `CUSTOM_ELEMENTS_SCHEMA` to your component or module this is Angular's built-in way to allow custom elements:
773
+ Angular needs no plugin for props, events, or two-way binding. The directive is only needed for scoped slots:
682
774
 
683
775
  ```ts
684
- import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
776
+ import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular'
685
777
 
686
- // Standalone component (Angular 17+)
687
778
  @Component({
779
+ imports: [WccSlotsDirective, WccSlotDef],
688
780
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
689
- template: `<wcc-counter></wcc-counter>`
690
- })
691
- export class AppComponent {}
692
-
693
- // Or NgModule approach
694
- @NgModule({
695
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
781
+ template: `
782
+ <wcc-card wccSlots>
783
+ <ng-template slot="header"><strong>Title</strong></ng-template>
784
+ <ng-template slot="stats" let-likes>{{ likes }} likes</ng-template>
785
+ </wcc-card>
786
+ `
696
787
  })
697
- export class AppModule {}
698
788
  ```
699
789
 
700
790
  ### Vanilla
package/adapters/react.js CHANGED
@@ -143,12 +143,15 @@ function toReactEventProp(eventName) {
143
143
  * Event names are converted: 'count-changed' → onCountChange prop (React convention)
144
144
  * @param {string[]} [config.models] - Model prop names for two-way binding
145
145
  * Each model 'name' creates: `name` prop (sets attribute) + `onNameChange` event
146
- * @returns {import('react').ForwardRefExoticComponent} A React component
146
+ * @param {string[]} [config.slots] - Named slot names for compound sub-components
147
+ * Each slot 'name' creates a `.Name` sub-component that renders `<div slot="name">`
148
+ * @returns {import('react').ForwardRefExoticComponent} A React component with compound sub-components
147
149
  *
148
150
  * @example
149
151
  * const WccCounter = createWccWrapper('wcc-counter', {
150
152
  * events: ['change'],
151
- * models: ['count']
153
+ * models: ['count'],
154
+ * slots: ['header', 'footer']
152
155
  * })
153
156
  *
154
157
  * function App() {
@@ -160,13 +163,15 @@ function toReactEventProp(eventName) {
160
163
  * onChange={(value) => console.log('changed', value)}
161
164
  * label="Clicks"
162
165
  * >
163
- * <div slot="footer">Footer content</div>
166
+ * <WccCounter.Header><strong>Title</strong></WccCounter.Header>
167
+ * <p>Body content</p>
168
+ * <WccCounter.Footer>Footer text</WccCounter.Footer>
164
169
  * </WccCounter>
165
170
  * )
166
171
  * }
167
172
  */
168
173
  export function createWccWrapper(tagName, config = {}) {
169
- const { events = [], models = [] } = config
174
+ const { events = [], models = [], slots = [] } = config
170
175
 
171
176
  // Build a set of event prop names for quick lookup
172
177
  // Convention: kebab-case event → React onCamelCase (without trailing 'd' from 'changed')
@@ -270,6 +275,24 @@ export function createWccWrapper(tagName, config = {}) {
270
275
 
271
276
  WccWrapper.displayName = tagName.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
272
277
 
278
+ // Compound components: generate .SlotName sub-components for each named slot
279
+ // This enables the pattern: <WccLayout.Header>content</WccLayout.Header>
280
+ // which renders as: <div slot="header">content</div>
281
+ for (const slotName of slots) {
282
+ if (!slotName) continue // skip default slot
283
+ const pascalSlot = slotName[0].toUpperCase() + slotName.slice(1)
284
+ const SlotComponent = function WccSlot({ children, ...rest }) {
285
+ const slotProps = { slot: slotName, style: { display: 'contents' } }
286
+ // Pass through any extra props as attributes on the wrapper div
287
+ for (const [key, value] of Object.entries(rest)) {
288
+ slotProps[key] = value
289
+ }
290
+ return React.createElement('div', slotProps, children)
291
+ }
292
+ SlotComponent.displayName = `${WccWrapper.displayName}.${pascalSlot}`
293
+ WccWrapper[pascalSlot] = SlotComponent
294
+ }
295
+
273
296
  return WccWrapper
274
297
  }
275
298
 
@@ -304,6 +327,7 @@ export function wrapWccComponent(WccClass) {
304
327
  return createWccWrapper(meta.tag, {
305
328
  events: meta.events || [],
306
329
  models: meta.models || [],
330
+ slots: meta.slots || [],
307
331
  })
308
332
  }
309
333
 
@@ -326,8 +350,12 @@ export function wrapWccComponent(WccClass) {
326
350
  * export const { WccCounter, WccCard } = createWccWrappers()
327
351
  *
328
352
  * // Then use anywhere:
329
- * <WccCounter count={count} onCountChanged={setCount} />
330
- * <WccCard><div slot="header">Title</div></WccCard>
353
+ * <WccCounter count={count} onCountChange={setCount} />
354
+ * <WccCard>
355
+ * <WccCard.Header><strong>Title</strong></WccCard.Header>
356
+ * <p>Body</p>
357
+ * <WccCard.Footer>Footer</WccCard.Footer>
358
+ * </WccCard>
331
359
  */
332
360
  export function createWccWrappers(options = {}) {
333
361
  const { prefix = 'wcc-' } = options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
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": {