` // Set classList property
// Property binding sets both property AND attribute (unless object/boolean)
```
**Special Cases:**
```javascript
// SVG support
html`
`
// Self-closing tags
html`

`
// Comments - preserved in template
html`
Title
`
// Doctype - only at root
html`...`
// Template tag
html`
${() => repeatContent()}`
// Slot handling
html`
${() => slottedContent}`
```
### Component System: `$.component(tagName, setupFn, observedAttributes)`
Full Web Components integration with reactive properties.
**Complete Component Lifecycle:**
```javascript
$.component('my-counter', (props, ctx) => {
// 1. Initialization phase
console.log('Component initializing');
// Local state (not exposed as attribute)
const internalCount = $(0);
// Computed from props
const doubled = $(() => props.count() * 2);
// 2. Setup side effects
const stopEffect = $.effect(() => {
console.log('Count changed:', props.count());
});
// 3. Register cleanup
ctx.onUnmount(() => {
console.log('Cleaning up');
stopEffect();
});
// 4. Access DOM
ctx.select('button')?.classList.add('primary');
// 5. Handle slots
const defaultSlot = ctx.slot(); // unnamed slots
const headerSlot = ctx.slot('header'); // named slots
// 6. Emit events
const handleClick = () => {
ctx.emit('increment', { value: props.count() });
ctx.emit('counter-changed', { count: props.count() }, { bubbles: false }); // Custom options
};
// 7. Return template (must return Node or DocumentFragment)
return html`
Default header
Default content
`;
}, ['count', 'min', 'max']); // Observed attributes
// Usage examples:
const el = document.createElement('my-counter');
el.count = 5; // Set property
el.setAttribute('min', '0'); // Set attribute
el.min = 10; // Property takes precedence
console.log(el.count); // Get property -> 10
console.log(el.getAttribute('count')); // Get attribute -> "10"
// Special attribute conversion:
el.disabled = ''; // true (except for 'value')
el.disabled = 'false'; // false
el.value = ''; // "" (empty string preserved)
el.checked = 'false'; // false
// Event listening:
el.addEventListener('increment', (e) => {
console.log(e.detail); // { value: x }
});
// Dynamic component creation:
const container = document.getElementById('app');
container.innerHTML = '
';
// Or programmatically:
const counter = new (customElements.get('my-counter'))();
counter.count = 5;
container.appendChild(counter);
```
**Advanced Component Patterns:**
```javascript
// Context/provider pattern
$.component('theme-provider', (props, ctx) => {
const theme = $(props.theme());
// Provide to children via custom event
ctx.emit('theme-provider', { theme });
return html`
`;
}, ['theme']);
$.component('themed-button', (props, ctx) => {
const theme = $('light');
// Consume from parent
ctx.host.addEventListener('theme-provider', (e) => {
theme(e.detail.theme);
});
return html`
`;
});
// Compound components
$.component('tabs', (props, ctx) => {
const activeTab = $(0);
return html`
${() => props.tabs()[activeTab()].content}
`;
}, ['tabs']);
// Form association
$.component('my-input', (props, ctx) => {
// Integrate with parent forms
const form = ctx.host.closest('form');
if (form) {
form.addEventListener('reset', () => {
props.value(props.defaultValue());
});
}
return html`
`;
}, ['value', 'defaultValue']);
```
### Persistent Storage: `$.storage(key, initialValue, storage?)`
Automatic persistence with change detection.
```javascript
// Basic localStorage
const settings = $.storage('app-settings', { theme: 'light' });
settings({ ...settings(), theme: 'dark' }); // Auto-saves
// Session storage
const tempData = $.storage('temp', null, sessionStorage);
// Multiple stores
const userPrefs = $.storage('user-prefs', {});
const appState = $.storage('app-state', {});
// Auto-cleanup
const cache = $.storage('cache', {});
cache(null); // Removes from storage
// Complex objects - automatic JSON serialization
const complex = $.storage('complex', {
date: new Date(),
regex: /pattern/,
nested: { array: [1,2,3] }
});
// Storage events - auto sync across tabs
window.addEventListener('storage', (e) => {
if (e.key === 'app-settings') {
// Signal auto-updates on next read
}
});
// Migration pattern
const data = $.storage('v2-data', () => {
// Migration from v1
const old = localStorage.getItem('v1-data');
return old ? migrate(JSON.parse(old)) : defaultValue;
});
// Computed from storage
const config = $(() => {
const base = settings();
return { ...base, computed: derive(base) };
});
```
### Router: `$.router(routes)` and `$.router.go()`
Hash-based routing with parameter extraction.
```javascript
// Route definitions - comprehensive examples
const router = $.router([
// Static routes
{ path: '/', component: () => html`
` },
{ path: '/about', component: () => html`
` },
// Parameterized routes
{
path: '/users/:id',
component: (params) => html`
`
},
{
path: '/posts/:postId/comments/:commentId',
component: (params) => html`
Post ${params.postId}, Comment ${params.commentId}
`
},
// Regex routes with capture groups
{
path: /^\/products\/(?
\w+)\/(?\d+)$/,
component: (params) => html`
`
},
{
path: /\/archive\/(\d{4})\/(\d{2})/,
component: (params) => html`Archive: ${params[0]}-${params[1]}`
},
// Query parameter handling (manual)
{
path: '/search',
component: () => {
const searchParams = new URLSearchParams(window.location.search);
return html``;
}
},
// Nested routers
{
path: '/dashboard',
component: () => {
return $.router([
{ path: '/', component: dashboardHome },
{ path: '/settings', component: dashboardSettings }
]);
}
},
// Guard pattern
{
path: '/admin',
component: (params) => {
if (!isAuthenticated()) {
$.router.go('/login');
return html``;
}
return html``;
}
},
// Catch-all / 404
{
path: /.*/,
component: () => html``
}
]);
// Navigation
$.router.go('/users/123'); // Basic
$.router.go('users/123'); // Auto-adds leading slash
$.router.go('/search?q=test'); // Query strings preserved
// Advanced navigation
const navigateWithState = (path, state) => {
history.replaceState(state, '', `#${path}`);
window.dispatchEvent(new HashChangeEvent('hashchange'));
};
// Route change detection
window.addEventListener('hashchange', () => {
console.log('Route changed to:', window.location.hash);
});
// Current route signal (if needed)
const currentRoute = $(() => window.location.hash.replace(/^#/, '') || '/');
// Route params as signals
const routeParams = $(null);
$.effect(() => {
const match = currentRoute().match(/\/users\/(\d+)/);
routeParams(match ? { id: match[1] } : null);
});
// Lazy loading routes
const router = $.router([
{
path: '/heavy',
component: async (params) => {
const module = await import('./heavy-page.js');
return module.default(params);
}
}
]);
```
### Fetch: `$.fetch(url, data, loading?)`
Simplified data fetching with loading state.
```javascript
// Basic usage
const result = await $.fetch('/api/users', { id: 123 });
// With loading signal
const isLoading = $(false);
const data = await $.fetch('/api/search', { query: 'test' }, isLoading);
console.log(isLoading()); // false after completion
// Error handling (returns null on error)
const result = await $.fetch('/api/fail', {}) || fallbackData;
// Integration with signals
const userData = $(null);
const loading = $(false);
async function loadUser(id) {
userData(await $.fetch(`/api/users/${id}`, {}, loading));
}
// Reactive fetch with effect
$.effect(() => {
const id = currentUserId();
loadUser(id);
});
// Retry logic wrapper
async function fetchWithRetry(url, data, retries = 3) {
for (let i = 0; i < retries; i++) {
const result = await $.fetch(url, data);
if (result) return result;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
return null;
}
// Request cancellation
let abortController = null;
$.effect(() => {
if (abortController) abortController.abort();
abortController = new AbortController();
const id = userId();
fetch(`/api/user/${id}`, { signal: abortController.signal })
.then(r => r.json())
.then(data => userData(data));
});
```
### WebSocket: `$.ws(url, options)`
Full-featured WebSocket client with reactive state.
```javascript
// Basic connection
const ws = $.ws('wss://api.example.com/ws');
// With options
const ws = $.ws('wss://api.example.com/ws', {
reconnect: true,
maxReconnect: 10,
reconnectInterval: 2000 // Base interval for exponential backoff
});
// Reactive state
$.effect(() => {
console.log('Status:', ws.status()); // 'connecting', 'connected', 'disconnected', 'error'
console.log('Messages:', ws.messages()); // Array of received messages
console.log('Error:', ws.error()); // Last error or null
});
// Sending messages
ws.send({ type: 'join', room: 'general' });
ws.send('plain text message');
// Auto-reconnect with backoff
// Attempts: 2s, 4s, 8s, 16s, 32s (until maxReconnect)
// Message filtering
const commands = $(() => ws.messages().filter(m => m.type === 'command'));
const events = $(() => ws.messages().filter(m => m.type === 'event'));
// Send with acknowledgment
async function sendWithAck(data, timeout = 5000) {
return new Promise((resolve, reject) => {
const id = Math.random();
const message = { ...data, id };
const handler = (msg) => {
if (msg.ack === id) {
ws.messages.off(handler);
resolve(msg);
}
};
// Need message event listener pattern
const timeoutId = setTimeout(() => {
ws.messages.off(handler);
reject(new Error('Ack timeout'));
}, timeout);
// Custom listener would be needed for this pattern
ws.send(message);
});
}
// Reconnection handling
$.effect(() => {
if (ws.status() === 'connected') {
console.log('Connected, sending init...');
ws.send({ type: 'init', token: authToken() });
}
});
// Binary data
const ws = $.ws('wss://example.com/binary');
ws.send(new Blob([data]));
ws.send(new ArrayBuffer(8));
// Heartbeat / ping-pong
$.effect(() => {
if (ws.status() !== 'connected') return;
const interval = setInterval(() => {
ws.send({ type: 'ping' });
}, 30000);
return () => clearInterval(interval);
});
// Queue messages while disconnected
const messageQueue = $([]);
$.effect(() => {
if (ws.status() === 'connected') {
messageQueue().forEach(msg => ws.send(msg));
messageQueue([]);
}
});
function sendOrQueue(data) {
if (ws.status() === 'connected') {
ws.send(data);
} else {
messageQueue([...messageQueue(), data]);
}
}
```
### Advanced Patterns & Best Practices
**State Management Patterns:**
```javascript
// Store pattern
const createStore = (initial) => {
const state = $(initial);
return {
state,
actions: {
update: (fn) => state(fn(state())),
reset: () => state(initial)
}
};
};
// Selector pattern
const selectUser = (id) => $(() =>
users().find(u => u.id === id)
);
// Computed selector
const selectVisibleTodos = $(() =>
todos().filter(t =>
filter() === 'all' ||
(filter() === 'active' && !t.completed) ||
(filter() === 'completed' && t.completed)
)
);
// Action pattern
const increment = () => count(c => c + 1);
const addTodo = (text) => todos(t => [...t, { text, completed: false }]);
const toggleTodo = (id) => todos(t => t.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
```
**Performance Optimization:**
```javascript
// Memoization
const expensiveComputation = $(() => {
console.log('Computing...');
return bigData().filter(heavyFilter).map(transform);
});
// Batch updates
count(1);
count(2);
count(3); // Single effect run
// Manual flush if needed
import { flushEffectQueue } from 'sigpro';
flushEffectQueue(); // Force immediate effect processing
// Lazy computed - only computes when accessed
const lazyValue = $(() => {
if (!cache) cache = expensive();
return cache;
});
// Effect debouncing
let timeout;
$.effect(() => {
clearTimeout(timeout);
timeout = setTimeout(() => {
console.log('Debounced:', value());
}, 100);
});
```
**Error Boundaries & Recovery:**
```javascript
// Effect error handling
$.effect(() => {
try {
riskyOperation();
} catch (e) {
errorSignal(e);
}
});
// Component error boundary
$.component('error-boundary', (props, ctx) => {
const error = $(null);
const handleError = (e) => {
error(e.error);
e.preventDefault();
};
ctx.host.addEventListener('error', handleError);
ctx.onUnmount(() => ctx.host.removeEventListener('error', handleError));
return () => error()
? html``
: html``;
});
// Global error handler
window.addEventListener('error', (e) => {
console.error('SigPro error:', e.error);
});
```
**Testing Utilities:**
```javascript
// Test helper pattern
const createTestHarness = () => {
const effects = [];
const origEffect = $.effect;
$.effect = (fn) => {
const stop = origEffect(fn);
effects.push(stop);
return stop;
};
return {
cleanup: () => effects.forEach(stop => stop()),
restore: () => { $.effect = origEffect; }
};
};
// Async test helper
const waitForEffect = () => new Promise(r => setTimeout(r, 0));
// Usage in tests
test('counter increments', async () => {
const count = $(0);
const calls = [];
$.effect(() => {
calls.push(count());
});
count(1);
await waitForEffect(); // Wait for effect queue
expect(calls).toEqual([0, 1]);
});
```
**TypeScript Integration (conceptual):**
```typescript
// Type patterns (even though library is JS)
type Signal = {
(): T;
(value: T | ((prev: T) => T)): void;
};
type Computed = () => T;
type Effect = (fn: () => (void | (() => void))) => () => void;
// Component props typing
interface ComponentProps {
[key: string]: Signal;
}
interface ComponentContext {
select: (selector: string) => Element | null;
slot: (name?: string) => Node[];
emit: (name: string, detail?: any, options?: CustomEventInit) => void;
host: HTMLElement;
onUnmount: (fn: () => void) => void;
}
```
**Migration from Other Frameworks:**
```javascript
// From Vue:
// ref(0) -> $(0)
// computed -> $(() => value)
// watch -> $.effect
// onMounted -> $.effect (runs immediately)
// From React:
// useState -> $ (but get/set combined)
// useEffect -> $.effect
// useMemo -> $(() => value)
// useCallback -> Just use function, dependencies automatic
// From Svelte:
// let count = 0 -> const count = $(0)
// $: doubled = count * 2 -> const doubled = $(() => count())
```
## Internal Architecture (for deep understanding)
```javascript
// Reactivity graph structure
{
activeEffect: Effect | null, // Currently executing effect
effectQueue: Set, // Pending effects
signal: {
subscribers: Set, // Effects depending on this signal
value: any, // Current value
isDirty?: boolean // For computed signals
},
effect: {
dependencies: Set>, // Signals this effect depends on
cleanupHandlers: Set, // Cleanup functions
markDirty?: Function // For computed signal effects
}
}
// Flush mechanism
// 1. Signal update adds affected effects to queue
// 2. Schedules microtask flush
// 3. Effects run in order added
// 4. Each effect cleans up old dependencies
// 5. New dependencies tracked during run
```
## Complete Example Application
```javascript
// app.js
import { $, html, $.component, $.router, $.storage, $.ws } from 'sigpro';
// State
const todos = $.storage('todos', []);
const filter = $('all');
const user = $(null);
const ws = $.ws('wss://api.example.com/todos');
// Computed
const filteredTodos = $(() => {
const all = todos();
switch(filter()) {
case 'active': return all.filter(t => !t.completed);
case 'completed': return all.filter(t => t.completed);
default: return all;
}
});
const stats = $(() => {
const all = todos();
return {
total: all.length,
active: all.filter(t => !t.completed).length,
completed: all.filter(t => t.completed).length
};
});
// Actions
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
};
todos([...todos(), newTodo]);
ws.send({ type: 'add', todo: newTodo });
};
const toggleTodo = (id) => {
todos(todos().map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
const removeTodo = (id) => {
todos(todos().filter(t => t.id !== id));
};
// Sync with WebSocket
$.effect(() => {
ws.messages().forEach(msg => {
if (msg.type === 'sync') {
todos(msg.todos);
}
});
});
// Components
$.component('todo-app', () => html`
addTodo(e.detail)}>
filteredTodos()}
@toggle=${(e) => toggleTodo(e.detail)}
@remove=${(e) => removeTodo(e.detail)}
>
${() => ws.status() !== 'connected' ? html`
Offline - ${() => ws.status()}
` : ''}
`);
$.component('todo-input', (props, ctx) => {
const text = $('');
const handleSubmit = () => {
if (text().trim()) {
ctx.emit('add', text().trim());
text('');
}
};
return html`
`;
});
$.component('todo-list', (props) => html`
`, ['todos']);
$.component('todo-stats', (props) => html`
Total: ${() => props.stats().total}
Active: ${() => props.stats().active}
Completed: ${() => props.stats().completed}
`, ['stats']);
// Router setup
const router = $.router([
{ path: '/', component: () => html`` },
{ path: '/settings', component: () => html`` },
{ path: '/about', component: () => html`` }
]);
// Mount app
document.body.appendChild(router);
```
This comprehensive documentation should enable any AI to build sophisticated applications leveraging all of SigPro's capabilities. Each API is explained with multiple examples, edge cases, advanced patterns, and integration scenarios.