npm package: @fionoble/preact-custom-elements
Description: Framework for building Web Components with Preact
Author: fionoble
Latest version: 1.0.0

# Preact Custom Elements

A lightweight framework for building Web Components with Preact. Write your components in Preact and automatically convert them into standards-compliant Custom Elements.

## Features

- Write components using familiar Preact syntax
- Automatic attribute-to-props mapping
- Support for camelCase props and kebab-case attributes
- Custom event dispatching
- Shadow DOM support (configurable)
- TypeScript support
- Minimal overhead

## Installation

```bash
pnpm add preact-custom-elements preact
```

## Quick Start

### 1. Create a Preact Component

```tsx
import { register } from 'preact-custom-elements';
import { useState } from 'preact/hooks';

interface CounterProps {
  initialCount?: number;
  step?: number;
  onChange?: (count: number) => void;
}

function Counter({ initialCount = 0, step = 1, onChange }: CounterProps) {
  const [count, setCount] = useState(initialCount);

  const increment = () => {
    const newCount = count + step;
    setCount(newCount);
    onChange?.(newCount);
  };

  return (
    <div>
      <h2>Count: {count}</h2>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

// Register as a custom element
register(Counter, 'my-counter', {
  attributes: ['initial-count', 'step'],
  shadow: true,
  events: ['change']
});
```

### 2. Use in HTML

```html
<my-counter initial-count="10" step="5"></my-counter>

<script>
  const counter = document.querySelector('my-counter');

  // Listen to custom events
  counter.addEventListener('change', (e) => {
    console.log('Count changed:', e.detail);
  });

  // Update properties dynamically
  counter.initialCount = 20;
</script>
```

## API

### `register(Component, tagName, options)`

Registers a Preact component as a Custom Element.

#### Parameters

- **Component** (`ComponentType<P>`): Your Preact component
- **tagName** (`string`): The custom element tag name (must contain a hyphen)
- **options** (`WebComponentOptions`): Configuration options

#### Options

```typescript
interface WebComponentOptions {
  // Attributes that will be observed and converted to props
  attributes?: string[];

  // Attributes to observe for changes (defaults to attributes)
  observedAttributes?: string[];

  // Enable Shadow DOM (true, false, or ShadowRootInit)
  shadow?: boolean | ShadowRootInit;

  // Event names to dispatch as CustomEvents
  events?: string[];
}
```

## Features Explained

### Attribute Mapping

Attributes are automatically converted from kebab-case to camelCase:

```html
<my-component initial-value="10" user-name="John"></my-component>
```

Maps to:

```tsx
<MyComponent initialValue={10} userName="John" />
```

### Type Conversion

Attribute values are automatically parsed:

- `"true"` / `"false"` → boolean
- `"123"` → number
- `'{"key": "value"}'` → object (JSON.parse)
- Other strings → string

### Shadow DOM

Control Shadow DOM behavior:

```tsx
// Enable with default settings
register(Component, 'my-element', { shadow: true });

// Disable Shadow DOM
register(Component, 'my-element', { shadow: false });

// Custom Shadow DOM config
register(Component, 'my-element', {
  shadow: { mode: 'closed', delegatesFocus: true }
});
```

### Custom Events

Components can dispatch custom events:

```tsx
function MyButton({ onClick }: { onClick?: (value: string) => void }) {
  return (
    <button onClick={() => onClick?.('clicked!')}>
      Click me
    </button>
  );
}

register(MyButton, 'my-button', {
  events: ['click']
});
```

```html
<my-button></my-button>

<script>
  document.querySelector('my-button')
    .addEventListener('click', (e) => {
      console.log(e.detail); // 'clicked!'
    });
</script>
```

### Slots

The framework supports both default and named slots when Shadow DOM is enabled. Slot content is automatically extracted from the host element and passed to your component via the `slots` prop.

#### Using Slots in Components

```tsx
import { Slot } from 'preact-custom-elements';

interface CardProps {
  slots?: {
    default?: any[];
    named: Record<string, any[]>;
  };
}

function Card({ slots }: CardProps) {
  const header = slots?.named?.header;
  const footer = slots?.named?.footer;

  return (
    <div class="card">
      {header && <div class="header">{header}</div>}

      <div class="content">
        {slots?.default || <Slot />}
      </div>

      {footer && <div class="footer">{footer}</div>}
    </div>
  );
}

register(Card, 'my-card', { shadow: true });
```

#### Using Slots in HTML

```html
<my-card>
  <h2 slot="header">Card Title</h2>

  <p>This goes in the default slot.</p>
  <p>Multiple elements are supported!</p>

  <div slot="footer">Footer content</div>
</my-card>
```

**Key Features:**
- **Automatic Extraction**: Slot content is automatically extracted from the host element
- **Named Slots**: Use `slot="name"` attribute to target named slots
- **Default Slot**: Content without a slot attribute goes to the default slot
- **Reactive Updates**: Slot content automatically updates when children change
- **Shadow DOM Only**: Slots only work when Shadow DOM is enabled (`shadow: true`)

The `slots` prop structure:
```typescript
{
  default?: VNode[];      // Default slot content
  named: {                // Named slot content
    [slotName: string]: VNode[]
  }
}
```

## Examples

### Simple User Card

```tsx
interface UserCardProps {
  name?: string;
  email?: string;
  avatar?: string;
}

function UserCard({ name, email, avatar }: UserCardProps) {
  return (
    <div class="user-card">
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  );
}

register(UserCard, 'user-card', {
  attributes: ['name', 'email', 'avatar'],
  shadow: true
});
```

```html
<user-card
  name="Jane Doe"
  email="jane@example.com"
  avatar="https://example.com/avatar.jpg"
></user-card>
```

## Development

```bash
# Install dependencies
pnpm install

# Build the library
pnpm build

# Run example
pnpm example
```

## Build Configuration

The project includes Rollup configuration for building both CommonJS and ES modules:

```bash
pnpm build  # Creates dist/index.js and dist/index.esm.js
```

## TypeScript

Full TypeScript support with type definitions included. The framework infers prop types from your component.

## Browser Support

Works in all modern browsers that support:
- Custom Elements v1
- Shadow DOM v1
- ES2020

## License

MIT