# Components API 🧩 Components in SigPro are native Web Components built on the Custom Elements standard. They provide a way to create reusable, encapsulated pieces of UI with reactive properties and automatic cleanup. ## `$.component(tagName, setupFunction, observedAttributes, useShadowDOM)` Creates a custom element with reactive properties and automatic dependency tracking. ```javascript import { $, html } from 'sigpro'; $.component('my-button', (props, { slot, emit }) => { return html` `; }, ['variant']); // Observe the 'variant' attribute ``` ## 📋 API Reference ### Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `tagName` | `string` | required | Custom element tag name (must include a hyphen, e.g., `my-button`) | | `setupFunction` | `Function` | required | Function that returns the component's template | | `observedAttributes` | `string[]` | `[]` | Attributes to observe for changes (become reactive props) | | `useShadowDOM` | `boolean` | `false` | `true` = Shadow DOM (encapsulated), `false` = Light DOM (inherits styles) | ### Setup Function Parameters The setup function receives two arguments: 1. **`props`** - Object containing reactive signals for each observed attribute 2. **`context`** - Object with helper methods and properties #### Context Object Properties | Property | Type | Description | |----------|------|-------------| | `slot(name)` | `Function` | Returns array of child nodes for the specified slot | | `emit(name, detail)` | `Function` | Dispatches a custom event | | `select(selector)` | `Function` | Query selector within component's root | | `selectAll(selector)` | `Function` | Query selector all within component's root | | `host` | `HTMLElement` | Reference to the custom element instance | | `root` | `Node` | Component's root (shadow root or element itself) | | `onUnmount(callback)` | `Function` | Register cleanup function | ## 🏠 Light DOM vs Shadow DOM ### Light DOM (`useShadowDOM = false`) - Default The component **inherits global styles** from the application. Perfect for components that should integrate with your site's design system. ```javascript // Button that uses global Tailwind CSS $.component('tw-button', (props, { slot, emit }) => { const variant = props.variant() || 'primary'; const variants = { primary: 'bg-blue-500 hover:bg-blue-600 text-white', secondary: 'bg-gray-500 hover:bg-gray-600 text-white', outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50' }; return html` `; }, ['variant']); ``` ### Shadow DOM (`useShadowDOM = true`) - Encapsulated The component **encapsulates its styles** completely. External styles don't affect it, and its styles don't leak out. ```javascript // Calendar with encapsulated styles $.component('ui-calendar', (props) => { return html`
${renderCalendar(props.date())}
`; }, ['date'], true); // true = use Shadow DOM ``` ## 🎯 Basic Examples ### Simple Counter Component ```javascript // counter.js $.component('my-counter', (props) => { const count = $(0); return html`

Count: ${count}

`; }); ``` **Usage:** ```html ``` ### Component with Props ```javascript // greeting.js $.component('my-greeting', (props) => { const name = props.name() || 'World'; const greeting = $(() => `Hello, ${name}!`); return html`

${greeting}

This is a greeting component.

`; }, ['name']); // Observe the 'name' attribute ``` **Usage:** ```html ``` ### Component with Events ```javascript // toggle.js $.component('my-toggle', (props, { emit }) => { const isOn = $(props.initial() === 'on'); const toggle = () => { isOn(!isOn()); emit('toggle', { isOn: isOn() }); emit(isOn() ? 'on' : 'off'); }; return html` `; }, ['initial']); ``` **Usage:** ```html console.log('Toggled:', e.detail)} @on=${() => console.log('Turned on')} @off=${() => console.log('Turned off')} > ``` ## 🎨 Advanced Examples ### Form Input Component ```javascript // form-input.js $.component('form-input', (props, { emit }) => { const value = $(props.value() || ''); const error = $(null); const touched = $(false); // Validation effect $.effect(() => { if (props.pattern() && touched()) { const regex = new RegExp(props.pattern()); const isValid = regex.test(value()); error(isValid ? null : props.errorMessage() || 'Invalid input'); emit('validate', { isValid, value: value() }); } }); const handleInput = (e) => { value(e.target.value); emit('update', e.target.value); }; const handleBlur = () => { touched(true); }; return html`
${props.label() ? html` ` : ''} ${() => error() ? html`
${error()}
` : ''} ${props.helpText() ? html` ${props.helpText()} ` : ''}
`; }, ['label', 'type', 'value', 'placeholder', 'disabled', 'required', 'pattern', 'errorMessage', 'helpText']); ``` **Usage:** ```html formData.email = e.detail} @validate=${(e) => setEmailValid(e.detail.isValid)} > ``` ### Modal/Dialog Component ```javascript // modal.js $.component('my-modal', (props, { slot, emit, onUnmount }) => { const isOpen = $(false); // Handle escape key const handleKeydown = (e) => { if (e.key === 'Escape' && isOpen()) { close(); } }; $.effect(() => { if (isOpen()) { document.addEventListener('keydown', handleKeydown); document.body.style.overflow = 'hidden'; } else { document.removeEventListener('keydown', handleKeydown); document.body.style.overflow = ''; } }); // Cleanup on unmount onUnmount(() => { document.removeEventListener('keydown', handleKeydown); document.body.style.overflow = ''; }); const open = () => { isOpen(true); emit('open'); }; const close = () => { isOpen(false); emit('close'); }; // Expose methods to parent props.open = open; props.close = close; return html`
${() => isOpen() ? html` ` : ''}
`; }, ['title'], false); ``` **Usage:** ```html

Are you sure you want to delete this item?

This action cannot be undone.

``` ### Data Table Component ```javascript // data-table.js $.component('data-table', (props, { emit }) => { const data = $(props.data() || []); const columns = $(props.columns() || []); const sortColumn = $(null); const sortDirection = $('asc'); const filterText = $(''); // Computed: filtered and sorted data const processedData = $(() => { let result = [...data()]; // Filter if (filterText()) { const search = filterText().toLowerCase(); result = result.filter(row => Object.values(row).some(val => String(val).toLowerCase().includes(search) ) ); } // Sort if (sortColumn()) { const col = sortColumn(); const direction = sortDirection() === 'asc' ? 1 : -1; result.sort((a, b) => { if (a[col] < b[col]) return -direction; if (a[col] > b[col]) return direction; return 0; }); } return result; }); const handleSort = (col) => { if (sortColumn() === col) { sortDirection(sortDirection() === 'asc' ? 'desc' : 'asc'); } else { sortColumn(col); sortDirection('asc'); } emit('sort', { column: col, direction: sortDirection() }); }; return html`
${() => `${processedData().length} of ${data().length} records`}
${columns().map(col => html` `)} ${() => processedData().map(row => html` emit('row-click', row)}> ${columns().map(col => html` `)} `)}
handleSort(col.field)} class:sortable=${true} class:sorted=${() => sortColumn() === col.field} > ${col.label} ${() => sortColumn() === col.field ? html` ${sortDirection() === 'asc' ? '↑' : '↓'} ` : ''}
${row[col.field]}
${() => processedData().length === 0 ? html`
No data found
` : ''}
`; }, ['data', 'columns']); ``` **Usage:** ```javascript const userColumns = [ { field: 'id', label: 'ID' }, { field: 'name', label: 'Name' }, { field: 'email', label: 'Email' }, { field: 'role', label: 'Role' } ]; const userData = [ { id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' } ]; ``` ```html console.log('Row clicked:', e.detail)} > ``` ### Tabs Component ```javascript // tabs.js $.component('my-tabs', (props, { slot, emit }) => { const activeTab = $(props.active() || 0); // Get all tab headers from slots const tabs = $(() => { const headers = slot('tab'); return headers.map((node, index) => ({ index, title: node.textContent, content: slot(`panel-${index}`)[0] })); }); $.effect(() => { emit('change', { index: activeTab(), tab: tabs()[activeTab()] }); }); return html`
${tabs().map(tab => html` `)}
${tabs().map(tab => html`
${tab.content}
`)}
`; }, ['active']); ``` **Usage:** ```html console.log('Tab changed:', e.detail)}>
Profile

Profile Settings

...
Security

Security Settings

...
Notifications

Notification Preferences

...
``` ### Component with External Data ```javascript // user-profile.js $.component('user-profile', (props, { emit, onUnmount }) => { const user = $(null); const loading = $(false); const error = $(null); // Fetch user data when userId changes $.effect(() => { const userId = props.userId(); if (!userId) return; loading(true); error(null); const controller = new AbortController(); fetch(`/api/users/${userId}`, { signal: controller.signal }) .then(res => res.json()) .then(data => { user(data); emit('loaded', data); }) .catch(err => { if (err.name !== 'AbortError') { error(err.message); emit('error', err); } }) .finally(() => loading(false)); // Cleanup: abort fetch if component unmounts or userId changes onUnmount(() => controller.abort()); }); return html`
${() => loading() ? html`
Loading...
` : error() ? html`
Error: ${error()}
` : user() ? html`

${user().name}

${user().email}

Member since: ${new Date(user().joined).toLocaleDateString()}

` : html`
No user selected
`}
`; }, ['user-id']); ``` ## 📦 Component Libraries ### Building a Reusable Component Library ```javascript // components/index.js import { $, html } from 'sigpro'; // Button component export const Button = $.component('ui-button', (props, { slot, emit }) => { const variant = props.variant() || 'primary'; const size = props.size() || 'md'; const sizes = { sm: 'px-2 py-1 text-sm', md: 'px-4 py-2', lg: 'px-6 py-3 text-lg' }; const variants = { primary: 'bg-blue-500 hover:bg-blue-600 text-white', secondary: 'bg-gray-500 hover:bg-gray-600 text-white', danger: 'bg-red-500 hover:bg-red-600 text-white' }; return html` `; }, ['variant', 'size', 'disabled']); // Card component export const Card = $.component('ui-card', (props, { slot }) => { return html`
${props.title() ? html`

${props.title()}

` : ''}
${slot()}
${props.footer() ? html` ` : ''}
`; }, ['title']); // Badge component export const Badge = $.component('ui-badge', (props, { slot }) => { const type = props.type() || 'default'; const types = { default: 'bg-gray-100 text-gray-800', success: 'bg-green-100 text-green-800', warning: 'bg-yellow-100 text-yellow-800', error: 'bg-red-100 text-red-800', info: 'bg-blue-100 text-blue-800' }; return html` ${slot()} `; }, ['type']); export { $, html }; ``` **Usage:** ```javascript import { Button, Card, Badge } from './components/index.js'; // Use components anywhere const app = html`

This is a card component

New
`; ``` ## 🎯 Decision Guide: Light DOM vs Shadow DOM | Use Light DOM (`false`) when... | Use Shadow DOM (`true`) when... | |--------------------------------|-------------------------------| | Component is part of your main app | Building a UI library for others | | Using global CSS (Tailwind, Bootstrap) | Creating embeddable widgets | | Need to inherit theme variables | Styles must be pixel-perfect everywhere | | Working with existing design system | Component has complex, specific styles | | Quick prototyping | Distributing to different projects | | Form elements that should match site | Need style isolation/encapsulation | ## 📊 Summary | Feature | Description | |---------|-------------| | **Native Web Components** | Built on Custom Elements standard | | **Reactive Props** | Observed attributes become signals | | **Two Rendering Modes** | Light DOM (default) or Shadow DOM | | **Automatic Cleanup** | Effects and listeners cleaned up on disconnect | | **Event System** | Custom events with `emit()` | | **Slot Support** | Full slot API for content projection | | **Zero Dependencies** | Pure vanilla JavaScript | --- > **Pro Tip:** Start with Light DOM components for app-specific UI, and use Shadow DOM when building components that need to work identically across different projects or websites.