# 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`
emit('click')}
>
${slot()}
`;
}, ['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`
emit('click')}
>
${slot()}
`;
}, ['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}
count(c => c + 1)}>+
count(c => c - 1)}>-
count(0)}>Reset
`;
});
```
**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`
${() => isOn() ? 'ON' : 'OFF'}
`;
}, ['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`
`;
}, ['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`
${slot('trigger') || 'Open Modal'}
${() => isOpen() ? html`
` : ''}
`;
}, ['title'], false);
```
**Usage:**
```html
Delete Item
Are you sure you want to delete this item?
This action cannot be undone.
Cancel
Delete
```
### 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`
handleSort(col.field)}
class:sortable=${true}
class:sorted=${() => sortColumn() === col.field}
>
${col.label}
${() => sortColumn() === col.field ? html`
${sortDirection() === 'asc' ? '↑' : '↓'}
` : ''}
`)}
${() => processedData().map(row => html`
emit('row-click', row)}>
${columns().map(col => html`
${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`
${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`
emit('click')}
>
${slot()}
`;
}, ['variant', 'size', 'disabled']);
// Card component
export const Card = $.component('ui-card', (props, { slot }) => {
return html`
${props.title() ? html`
` : ''}
${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
Save Changes
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.