diff --git a/Readme.md b/Readme.md index 1bdae55..2bb4d90 100644 --- a/Readme.md +++ b/Readme.md @@ -572,30 +572,30 @@ const Page = () => html` --- -### `$component(tag, setup, observedAttributes)` - Web Components +### `$component(tagName, setupFunction, observedAttributes)` - Web Components -Creates Custom Elements with automatic reactive properties. +Creates Custom Elements with reactive properties. Uses Light DOM (no Shadow DOM) and a slot system based on node filtering. #### Basic Component -```typescript +```javascript import { $, $component, html } from 'sigpro'; $component('my-counter', (props, context) => { // props contains signals for each observed attribute - // context provides component utilities + // context: { slot, emit, host, onUnmount } const increment = () => { - props.value(v => v + 1); + props.value(v => (parseInt(v) || 0) + 1); }; return html` -
-

Count: ${props.value}

- - +
+

Value: ${props.value}

+ + + + ${context.slot()}
`; }, ['value']); // Observed attributes @@ -604,147 +604,36 @@ $component('my-counter', (props, context) => { Usage: ```html - Additional content + ▼ This is the default slot +

More content in the slot

``` -#### Component with Complex Props +#### Component with Named Slots -```typescript +```javascript import { $, $component, html } from 'sigpro'; -$component('user-profile', (props, context) => { - // Transform string attributes to appropriate types - const user = $(() => ({ - id: parseInt(props.id()), - name: props.name(), - age: parseInt(props.age()), - active: props.active() === 'true' - })); - +$component('my-card', (props, { slot }) => { return html` -
-

${user().name}

-

ID: ${user().id}

-

Age: ${user().age}

-

Status: ${() => user().active ? 'Active' : 'Inactive'}

+
+
+ ${slot('header')} +
- -
- `; -}, ['id', 'name', 'age', 'active']); -``` - -#### Component Lifecycle & Context - -```typescript -import { $, $component, html } from 'sigpro'; - -$component('lifecycle-demo', (props, { - select, // Query selector scoped to component - selectAll, // Query selector all scoped to component - slot, // Access slots - emit, // Dispatch custom events - host, // Reference to the host element - onMount, // Register mount callback - onUnmount, // Register unmount callback - onAttribute, // Listen to attribute changes - getAttribute, // Get raw attribute value - setAttribute, // Set raw attribute value -}) => { - - // Access slots - const defaultSlot = slot(); // Unnamed slot - const headerSlot = slot('header'); // Named slot - - // Query internal elements - const button = select('button'); - const allSpans = selectAll('span'); - - // Lifecycle hooks - onMount(() => { - console.log('Component mounted'); - // Access DOM after mount - button?.classList.add('mounted'); - }); - - onUnmount(() => { - console.log('Component unmounting'); - // Cleanup resources - }); - - // Listen to specific attribute changes - onAttribute('value', (newValue, oldValue) => { - console.log(`Value changed from ${oldValue} to ${newValue}`); - }); - - // Emit custom events - const handleClick = () => { - emit('button-click', { timestamp: Date.now() }); - emit('value-change', props.value(), { bubbles: true }); - }; - - // Access host directly - host.style.display = 'block'; - - return html` -
- ${headerSlot} - - ${defaultSlot} -
- `; -}, ['value']); -``` - -#### Component with Methods - -```typescript -import { $, $component, html } from 'sigpro'; - -$component('timer-widget', (props, { host }) => { - const seconds = $(0); - let intervalId; - - // Expose methods to the host element - Object.assign(host, { - start() { - if (intervalId) return; - intervalId = setInterval(() => { - seconds(s => s + 1); - }, 1000); - }, - - stop() { - clearInterval(intervalId); - intervalId = null; - }, - - reset() { - seconds(0); - }, - - get currentTime() { - return seconds(); - } - }); - - return html` -
-

${seconds} seconds

- - - +
+ ${slot()} +
+ +
`; }, []); @@ -752,41 +641,237 @@ $component('timer-widget', (props, { host }) => { Usage: ```html - - + +

Card Title

+ +

This goes to default slot

+ Also default slot + +
+ +
+
``` -#### Component Inheritance +#### Component with Props and Events -```typescript +```javascript import { $, $component, html } from 'sigpro'; -// Base component -$component('base-button', (props, { slot }) => { +$component('todo-item', (props, { emit, host }) => { + const handleToggle = () => { + props.completed(c => !c); + emit('toggle', { id: props.id(), completed: props.completed() }); + }; + + const handleDelete = () => { + emit('delete', { id: props.id() }); + }; + return html` - +
+ + props.completed() ? 'text-decoration: line-through' : ''}> + ${props.text} + + +
`; -}, ['disabled']); - -// Extended component -$component('primary-button', (props, context) => { - // Reuse base component - return html` - - ${context.slot()} - - `; -}, ['disabled']); +}, ['id', 'text', 'completed']); ``` ---- +Usage: +```html + console.log('Toggled:', e.detail)} + @delete=${(e) => console.log('Deleted:', e.detail)} +> +``` + +#### Component with Cleanup + +```javascript +import { $, $component, html, $$ } from 'sigpro'; + +$component('timer-widget', (props, { onUnmount }) => { + const seconds = $(0); + + // Effect with automatic cleanup + $$(() => { + const interval = setInterval(() => { + seconds(s => s + 1); + }, 1000); + + // Return cleanup function + return () => clearInterval(interval); + }); + + // Register unmount hook + onUnmount(() => { + console.log('Timer widget unmounted'); + }); + + return html` +
+

Seconds: ${seconds}

+

Initial value: ${props.initial}

+
+ `; +}, ['initial']); +``` + +#### Complete Context API + +```javascript +import { $, $component, html } from 'sigpro'; + +$component('context-demo', (props, context) => { + // Context properties: + // - slot(name) - Gets child nodes with matching slot attribute + // - emit(name, detail) - Dispatches custom event + // - host - Reference to the custom element instance + // - onUnmount(callback) - Register cleanup function + + const { + slot, // Function: (name?: string) => Node[] + emit, // Function: (name: string, detail?: any) => void + host, // HTMLElement: the custom element itself + onUnmount // Function: (callback: () => void) => void + } = context; + + // Access host directly + console.log('Host element:', host); + console.log('Host attributes:', host.getAttribute('my-attr')); + + // Handle events + const handleClick = () => { + emit('my-event', { message: 'Hello from component' }); + }; + + // Register cleanup + onUnmount(() => { + console.log('Cleaning up...'); + }); + + return html` +
+ ${slot('header')} + + ${slot()} + ${slot('footer')} +
+ `; +}, []); +``` + +#### Practical Example: Todo App Component + +```javascript +import { $, $component, html } from 'sigpro'; + +$component('todo-app', () => { + const todos = $([]); + const newTodo = $(''); + const filter = $('all'); + + const addTodo = () => { + if (newTodo().trim()) { + todos([...todos(), { + id: Date.now(), + text: newTodo(), + completed: false + }]); + newTodo(''); + } + }; + + const filteredTodos = $(() => { + const currentFilter = filter(); + const allTodos = todos(); + + if (currentFilter === 'active') { + return allTodos.filter(t => !t.completed); + } + if (currentFilter === 'completed') { + return allTodos.filter(t => t.completed); + } + return allTodos; + }); + + return html` +
+

📝 Todo App

+ + +
+ e.key === 'Enter' && addTodo()} + placeholder="What needs to be done?" + /> + +
+ + +
+ + + +
+ + +
+ ${() => filteredTodos().map(todo => html` + { + const { id, completed } = e.detail; + todos(todos().map(t => + t.id === id ? { ...t, completed } : t + )); + }} + @delete=${(e) => { + todos(todos().filter(t => t.id !== e.detail.id)); + }} + > + `)} +
+ + +
+ ${() => { + const total = todos().length; + const completed = todos().filter(t => t.completed).length; + return html` + Total: ${total} + Completed: ${completed} + Remaining: ${total - completed} + `; + }} +
+
+ `; +}, []); +``` + +#### Key Points About `$component`: + +1. **Light DOM only** - No Shadow DOM, children are accessible and styleable from outside +2. **Slot system** - `slot()` function filters child nodes by `slot` attribute +3. **Reactive props** - Each observed attribute becomes a signal in the `props` object +4. **Event emission** - `emit()` dispatches custom events with `detail` payload +5. **Cleanup** - `onUnmount()` registers functions called when component is removed +6. **Host access** - `host` gives direct access to the custom element instance + ### `$router(routes)` - Router @@ -1454,3 +1539,4 @@ function useFetch(url) { +