`;
}, ['value']); // Observed attributes
@@ -604,147 +604,36 @@ $component('my-counter', (props, context) => {
Usage:
```html
```
-#### 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'}
+
+
- props.age(a => parseInt(a) + 1)}>
- Birthday!
-
-
- `;
-}, ['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}
-
- Click me
-
- ${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
-
host.start()}>Start
-
host.stop()}>Stop
-
host.reset()}>Reset
+
+ ${slot()}
+
+
+
`;
}, []);
@@ -752,41 +641,237 @@ $component('timer-widget', (props, { host }) => {
Usage:
```html
-
-
+
+ Card Title
+
+ This goes to default slot
+ Also default slot
+
+
+ Action
+
+
```
-#### 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`
-
- ${slot()}
-
+
+
+ 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')}
+ Emit Event
+ ${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?"
+ />
+ Add
+
+
+
+
+ filter('all')}>All
+ filter('active')}>Active
+ filter('completed')}>Completed
+
+
+
+
+ ${() => 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) {
+