# @lilian1315/create-element

[![npm](https://img.shields.io/npm/v/@lilian1315/create-element)](https://npmx.dev/package/@lilian1315/create-element)
[![jsr](https://jsr.io/badges/@lilian1315/create-element)](https://jsr.io/@lilian1315/create-element)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

Type-safe DOM and virtual element creation with JSX, server rendering, and optional reactive library integrations.

## Features

- **Type-safe** element creation with full TypeScript autocompletion ([Usage](#usage))
- Support for **HTML, SVG, and MathML** elements ([SVG and MathML](#svg-and-mathml))
- Flexible attribute handling: classes, styles, datasets, events ([Attributes](#attributes))
- **JSX** support with Fragments and function components ([JSX Support](#jsx-support))
- Lightweight **virtual trees and server rendering** ([Virtual Trees and SSR](#virtual-trees-and-ssr))
- **Reactive adapters** for signal-based UI updates ([Reactive Support](#reactive-support-optional))

## Installation

```bash
pnpm add @lilian1315/create-element
```

## Usage

```typescript
import { h } from '@lilian1315/create-element'

// Create a simple element
const div = h('div', { class: 'container' }, 'Hello World!')

// With event handlers
const button = h('button', {
  onclick: () => console.log('Clicked!'),
  children: 'Click me',
})

// Nested elements
const app = h('div', null, [h('h1', null, 'My App'), button])
```

## API

### `h(tag, attributes?, ...children)`

Creates a DOM element and returns it.

| Parameter    | Type             | Description                                                                                          |
| ------------ | ---------------- | ---------------------------------------------------------------------------------------------------- |
| `tag`        | `string`         | HTML tag name (e.g. `'div'`), or prefixed for SVG/MathML (`'svg:circle'`, `'math:mi'`)               |
| `attributes` | `object \| null` | Optional attribute bag including special helpers (`class`, `style`, `data`, `innerHTML`, `children`) |
| `children`   | `Child[]`        | Additional child nodes — strings, numbers, DOM nodes, `null`, or arrays thereof                      |

**Returns:** the created DOM element (`HTMLElement`, `SVGElement`, or `MathMLElement`).

### Attributes

```typescript
// Classes — string, array, or conditional object
h('div', { class: 'btn primary' })
h('div', { class: ['btn', 'primary'] })
h('div', { class: { btn: true, primary: true, active: false } })

// Styles — string or object
h('div', { style: 'color: red' })
h('div', { style: { color: 'red', fontSize: '16px' } })

// Events — lowercase on* handlers
h('button', { onclick: () => console.log('clicked') })

// Data attributes — mapped to element.dataset
h('div', {
  data: {
    testId: 'my-component', // data-test-id="my-component"
    active: true, // data-active=""
    hidden: false, // removed
    count: null, // removed
    empty: undefined, // removed
  },
})

// innerHTML (mutually exclusive with children)
h('div', { innerHTML: '<span>content</span>' })
```

### SVG and MathML

Use `svg:` or `math:` prefixes for namespace-aware element creation:

```typescript
// SVG elements
const svg = h('svg', { width: '100', height: '100' })
const circle = h('svg:circle', { cx: '50', cy: '50', r: '20' })

// MathML elements
const math = h('math')
const variable = h('math:mi', null, 'x')
```

The root `svg` and `math` tags do not need a prefix.

## JSX Support

Configure TypeScript to use the JSX runtime:

```jsonc
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@lilian1315/create-element",
  },
}
```

### Elements and function components

```tsx
function Greeting({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>
}

function App() {
  return (
    <div class="container">
      <Greeting name="World" />
      <button onclick={() => console.log('clicked')}>Click me</button>
    </div>
  )
}

document.body.appendChild(App())
```

### Fragments

Use `Fragment` to group children without an extra DOM wrapper. A Fragment returns a `Node[]`.

```tsx
import { Fragment } from '@lilian1315/create-element/jsx-runtime'

function List() {
  return (
    <>
      <li>One</li>
      <li>Two</li>
    </>
  )
}
```

## Virtual Trees and SSR

The `/virtual` entry point uses the same `h(type, props, ...children)` API but returns lightweight
Preact-style VNodes instead of creating DOM elements immediately. `mount` materializes a tree in
the browser, while the separate `/server` entry point serializes the same tree without requiring a DOM.
This is a virtual tree API, not a reconciler: mounting materializes the complete tree, and VNode keys
are currently metadata only.

```typescript
import { h, mount } from '@lilian1315/create-element/virtual'
import { renderToString } from '@lilian1315/create-element/server'

const app = h('main', { class: 'page' }, h('h1', null, 'Hello from a VNode'))

const html = renderToString(app)
// <main class="page"><h1>Hello from a VNode</h1></main>

mount(document.querySelector<HTMLElement>('#app')!, app)
```

For virtual JSX, use the virtual runtime:

```jsonc
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@lilian1315/create-element/virtual",
  },
}
```

`renderToString` is synchronous and produces static HTML. Event handlers are omitted. `innerHTML`
is emitted without escaping and must only receive trusted or sanitized content. Client hydration is
not currently included.

`createElementFromVNode` materializes one intrinsic-element VNode. Components and fragments can
produce several nodes, so pass those trees to `mount` instead.

### Reactive server rendering

Append `/virtual` to an adapter path to create reactive VNodes, then use the corresponding `/server`
renderer to serialize their current values:

```typescript
import { shallowRef } from '@vue/reactivity'
import { renderToString } from '@lilian1315/create-element/vue-reactivity/server'
import { h, mount } from '@lilian1315/create-element/vue-reactivity/virtual'

const count = shallowRef(1)
const tree = h('p', null, 'Count: ', count)

renderToString(tree) // <p>Count: 1</p>
const dispose = mount(document.querySelector<HTMLElement>('#app')!, tree)

dispose()
```

The same `/virtual` and `/server` pair is available for `alien-signals`, `alien-deepsignals`,
`faisceau`, `preact-signals`, and `vue-reactivity`. Reactive `mount` subscribes to reactive values and
returns a disposer that stops every owned effect, removes event listeners, and clears the target.
Mounting again into the same target disposes its previous tree automatically. Server rendering reads
sources with `peek`, produces one static snapshot, and does not subscribe to later updates. For
reactive VNode JSX, use the adapter's `/virtual` path as `jsxImportSource`.

### Reactive JSX

For reactive JSX, set `jsxImportSource` to the adapter path:

| Signal library       | `jsxImportSource`                              |
| -------------------- | ---------------------------------------------- |
| alien-signals        | `@lilian1315/create-element/alien-signals`     |
| alien-deepsignals    | `@lilian1315/create-element/alien-deepsignals` |
| faisceau             | `@lilian1315/create-element/faisceau`          |
| @preact/signals-core | `@lilian1315/create-element/preact-signals`    |
| @vue/reactivity      | `@lilian1315/create-element/vue-reactivity`    |

## Precise JSX element types

TypeScript assigns `JSX.Element` to every JSX expression, even when this runtime creates a more
specific DOM element. Use `asDom` to state the concrete type:

```tsx
import { asDom } from '@lilian1315/create-element'

const container = asDom<'div'>(<div />)
const path = asDom<'svg:path'>(<svg:path />)
```

The accompanying Oxlint plugin verifies that the explicit type argument matches the intrinsic JSX
tag. It also checks direct assertions such as `<div /> as HTMLDivElement` and can fix mismatches.

```json
{
  "jsPlugins": ["@lilian1315/oxlint-plugin-create-element"],
  "rules": {
    "create-element/valid-jsx-type-assertion": "error"
  }
}
```

The rule is syntax-based. Type aliases and qualified type names are ignored because Oxlint JS
plugins do not expose TypeScript type information.

## Reactive Support (Optional)

Each reactive adapter wraps `createElement` so that signal/computed values in attributes, styles, datasets, and children are automatically tracked and updated in the DOM.

Import `h` from the adapter that matches your signal library and install the corresponding peer dependency.

### alien-signals

```typescript
import { h } from '@lilian1315/create-element/alien-signals'
import { computed, signal } from 'alien-signals'

const count = signal(0)
const label = computed(() => `Count: ${count()}`)

const counter = h('section', { class: 'counter' }, [
  h('p', null, label),
  h('button', { onclick: () => count(count() + 1) }, 'Increment'),
])
```

Requires: `pnpm add alien-signals`

### alien-deepsignals

```typescript
import { h } from '@lilian1315/create-element/alien-deepsignals'
import { computed, signal } from 'alien-deepsignals'

const count = signal(0)
const label = computed(() => `Count: ${count.get()}`)

const counter = h('section', null, [
  h('p', null, label),
  h('button', { onclick: () => count.set(count.get() + 1) }, 'Increment'),
])
```

Requires: `pnpm add alien-deepsignals`

### faisceau

```typescript
import { h } from '@lilian1315/create-element/faisceau'
import { computed, signal } from 'faisceau'

const count = signal(0)
const label = computed(() => `Count: ${count.get()}`)

const counter = h('section', null, [
  h('p', null, label),
  h('button', { onclick: () => count.set(count.get() + 1) }, 'Increment'),
])
```

Requires: `pnpm add faisceau`

### @preact/signals-core

```typescript
import { h } from '@lilian1315/create-element/preact-signals'
import { computed, signal } from '@preact/signals-core'

const count = signal(0)
const label = computed(() => `Count: ${count.value}`)

const counter = h('section', null, [
  h('p', null, label),
  h('button', { onclick: () => (count.value = count.value + 1) }, 'Increment'),
])
```

Requires: `pnpm add @preact/signals-core`

### @vue/reactivity

```typescript
import { h } from '@lilian1315/create-element/vue-reactivity'
import { computed, ref } from '@vue/reactivity'

const count = ref(0)
const label = computed(() => `Count: ${count.value}`)

const counter = h('section', null, [
  h('p', null, label),
  h('button', { onclick: () => count.value++ }, 'Increment'),
])
```

Requires: `pnpm add @vue/reactivity`

## License

MIT
