@nativele/ts-dom 0.4.6 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: nativele-core
3
+ description: "Build and change web components with Nativele (@nativele/ts-dom): the write-check-fix loop, templates, state, lists and data flow. Use whenever a project depends on @nativele/ts-dom and you add or change an .html template or a component."
4
+ metadata:
5
+ tier: official
6
+ nativele: ">=0.5.0 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "typescript@7.0.2"
9
+ author: "Nativele maintainers"
10
+ detects: "@nativele/ts-dom"
11
+ ---
12
+
13
+ # Nativele core
14
+
15
+ ## When to use
16
+
17
+ - The project depends on `@nativele/ts-dom` and you write or change a component: an `x.html` template with its `x.ts` class.
18
+ - Not for using Nativele components from a framework (React, Vue, ...): see the integration Skills (`npx ts-dom skills`), which build on this one.
19
+
20
+ A component is two files. The HTML holds structure and names (`{{ name }}`, `data-ref`, `data-if`, `data-each`, `data-on-*`), never expressions; the TypeScript class holds state and behavior. `ts-dom gen` turns each `x.html` into a typed `x.html.ts`, and `tsc` checks that the two files agree.
21
+
22
+ ## Versions
23
+
24
+ | Tool | Verified version |
25
+ |---|---|
26
+ | @nativele/ts-dom | 0.5.0 |
27
+ | typescript | 7.0.2 |
28
+
29
+ Verified on 2026-11-05 by the library's own CI, which type-checks, lints and runs every example in the repository in real Chrome.
30
+
31
+ ## Set-up
32
+
33
+ 1. `npm i @nativele/ts-dom` and `npm i -D typescript`, and set `"type": "module"` in `package.json` (with or without a bundler).
34
+ 2. Put the components under `src/`, the folder every `ts-dom` command scans by default; for another folder, pass it to each command (`npx ts-dom check web/ui`).
35
+ 3. `tsconfig.json` with `"strict": true`, `"lib"` including `"DOM"`, and `"moduleResolution"` `"NodeNext"` or `"bundler"`; its `include` must cover the folders with templates. A complete one for the no-bundler case is in the setup guide: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md#installation-and-setup
36
+ 4. With Vite, add `tsDom()` from `@nativele/ts-dom/vite` to `plugins`. Without a bundler, `ts-dom gen` plus `tsc` compiles the code, and the page needs an import map for `@nativele/ts-dom` plus a `<script type="module">` for the compiled entry, a module that imports the top-level components: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md#running-in-the-browser-no-bundler
37
+ 5. Add `*.html.ts` and `ts-dom-tags.ts` to `.gitignore`: they are generated.
38
+ 6. Run `npx ts-dom doctor` once; fix every error and warning it reports. Its `info` lines name the other Skills that fit the project.
39
+
40
+ ## Example
41
+
42
+ A list whose items are components, with a store, a derived value and events from the items:
43
+
44
+ ```html
45
+ <!-- src/todo-item.html -->
46
+ <input type="checkbox" data-ref="check" data-on-change="onToggle" />
47
+ <span>{{ text }}</span>
48
+ <button type="button" data-on-click="onRemove">remove</button>
49
+ ```
50
+
51
+ ```ts
52
+ // src/todo-item.ts
53
+ import { Component, define, event } from "@nativele/ts-dom";
54
+ import tpl from "./todo-item.html.js";
55
+ import type { Todo } from "./store.js";
56
+
57
+ export class TodoItem extends Component(tpl, {
58
+ props: { todo: { id: 0, title: "", done: false } as Todo },
59
+ derive: { text: (v) => v.todo.title },
60
+ emits: { toggled: event(), removed: event() },
61
+ }) {
62
+ onToggle(): void {
63
+ this.emit("toggled");
64
+ }
65
+ onRemove(): void {
66
+ this.emit("removed");
67
+ }
68
+ override render(view: { todo: Todo }, changed: ReadonlySet<"todo">): void {
69
+ if (changed.has("todo")) this.refs.check.checked = view.todo.done;
70
+ }
71
+ }
72
+ define("todo-item", TodoItem);
73
+ ```
74
+
75
+ ```html
76
+ <!-- src/todo-app.html -->
77
+ <p data-if="empty">Nothing to do</p>
78
+ <ul>
79
+ <template data-each="items" data-key="id" data-prop="todo">
80
+ <todo-item role="listitem" data-on-toggled="toggle" data-on-removed="drop"></todo-item>
81
+ </template>
82
+ </ul>
83
+ <p>{{ remaining }} left</p>
84
+ ```
85
+
86
+ ```ts
87
+ // src/todo-app.ts
88
+ import { Component, define, type EventHit } from "@nativele/ts-dom";
89
+ import tpl from "./todo-app.html.js";
90
+ import { todos, type Todo } from "./store.js"; // export const todos = createStore<readonly Todo[]>([]);
91
+ import "./todo-item.js";
92
+
93
+ export class TodoApp extends Component(tpl, {
94
+ state: { items: [] as readonly Todo[] },
95
+ derive: {
96
+ remaining: (v) => v.items.filter((t) => !t.done).length,
97
+ empty: (v) => v.items.length === 0,
98
+ },
99
+ }) {
100
+ override connected(): void {
101
+ this.bind(todos, (items) => this.set({ items }));
102
+ }
103
+ toggle(_e: Event, hit: EventHit<Todo, "id">): void {
104
+ todos.set((s) => s.map((t) => (t.id === hit.key ? { ...t, done: !t.done } : t)));
105
+ }
106
+ drop(_e: Event, hit: EventHit<Todo, "id">): void {
107
+ todos.set((s) => s.filter((t) => t.id !== hit.key));
108
+ }
109
+ override render(view: { items: readonly Todo[] }, changed: ReadonlySet<"items">): void {
110
+ if (changed.has("items")) this.lists.items.items = view.items;
111
+ }
112
+ }
113
+ define("todo-app", TodoApp);
114
+ ```
115
+
116
+ - A `derive` function takes one argument: the state and props merged into one read-only object (other derived values are not in it).
117
+ - `emits` declares each event with `event()`, or `event<Detail>()` when it carries data; send the data with `this.emit("name", detail)`. The parent listens with `data-on-<event>` on the child tag and reads the data as `(e as EmitEvent<Child, "name">).detail`, because an HTML attribute cannot carry the event's type.
118
+ - `bind` goes in `connected()`; the subscription is released when the element is removed.
119
+
120
+ ## The loop
121
+
122
+ 1. Write the `.html` and the `.ts` of a component together. Import the template as `./x.html.js`.
123
+ 2. Run `npx ts-dom check` (add `--json` to read the result as data). It regenerates the generated files, runs `tsc`, points template contract errors at the template line and column, and runs the lint rules. Use `--no-write` where the working tree must not change.
124
+ 3. Fix every issue by its `Fix:` hint. Never cast a type error away, and never edit `x.html.ts` or `ts-dom-tags.ts`: they are generated.
125
+ 4. `npx ts-dom rules --explain=<id>` explains any rule id from a diagnostic, with a wrong and a right example.
126
+ 5. Repeat until `ts-dom check` prints `no issues`.
127
+
128
+ ## Rules
129
+
130
+ - No expressions in HTML: `{{ name }}` is a single identifier. Compute in `derive` (or in TypeScript) and fill a named hole.
131
+ - Change state only with `this.set()`. Rendering is synchronous; `this.batch()` combines updates.
132
+ - Once a component has `state`, `props` or `derive`, every `{{ hole }}` and `data-if` outside a `<template data-each>` must name one of them (holes inside a plain-element list are filled by `replace`). Show store data by copying it into state in the `bind` callback.
133
+ - `store.set` replaces the whole value; a component's `this.set` merges. Spread the rest yourself: `store.set((s) => ({ ...s, status: "loading" }))`.
134
+ - Lists, attributes and focus go in `render(view, changed)`. `changed` holds the state, prop and derived names that changed; annotate it with the names you check (`changed: ReadonlySet<"items" | "visible">`), never `ReadonlySet<string>`.
135
+ - Props go down (the parent sets them), events go up (`emit`, handled with `data-on-<event>` on the child tag). `emit` does not bubble; data needed several levels away goes in a shared `createStore` that the components `bind`.
136
+ - State, props and the view are deeply read-only: replace objects instead of changing them (`{ ...todo, done: true }`).
137
+ - A `data-each` list cannot be nested: the inner list belongs to its own item component.
138
+ - A list of plain elements (no item component) has neither `data-key` nor `data-prop`; fill it with `this.lists.x.replace(items, key, fill)` in `render`: https://github.com/Pixmi/nativele/blob/main/docs/lists.md
139
+ - For a list of components, write `data-key` and `data-prop` on the `<template data-each>`, give the item component `role="listitem"` inside `<ul>` or `<ol>`, assign `this.lists.x.items = ...` in `render`, and type item handlers as `hit: EventHit<Item, "id">`. Each item component gets its item through that one prop; to pass more (selected, leading), map the items to objects that carry it before assigning.
140
+ - Do not name a handler or prop like a DOM or Nativele member (`remove`, `click`, `title`, `text`, `refs`, `state`, `render`, ...), or an emitted event like a native event (`click`, `change`, `input`, ...); `ts-dom check` reports both.
141
+
142
+ ## Known limitations
143
+
144
+ - Holes are text only; attributes are set in TypeScript through `data-ref`.
145
+ - A derived value cannot read another derived value; share the calculation as a plain function.
146
+ - `data-if` removes the element from the page (its state is kept in memory); to hide an element while it keeps focus or media playback, set `this.refs.x.hidden` in `render`.
147
+ - Nativele has no router, data fetching, form validation library, styling system or global state manager by design; use the project's tools for those, and see the other Skills for how they connect.
148
+
149
+ ## Verify
150
+
151
+ ```sh
152
+ npx ts-dom check --strict # expect: ts-dom check: no issues
153
+ npx ts-dom doctor # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
154
+ npx tsc --noEmit # expect: no errors
155
+ ```
156
+
157
+ Then exercise the changed component in a real browser (the project's own test runner, or the browser).
158
+
159
+ ## Troubleshooting
160
+
161
+ | Symptom | Cause | Fix |
162
+ |---|---|---|
163
+ | `ReferenceError: exports is not defined` in the browser | `package.json` has no `"type": "module"`, so `tsc` emitted CommonJS | set `"type": "module"`, delete the output folder, run `tsc` again |
164
+ | `Failed to resolve module specifier "@nativele/ts-dom"` | no bundler and no import map | add the import map from the setup guide before the module script |
165
+ | `Cannot find module './x.html.js'` | the generated file is missing | run `npx ts-dom check` (or `ts-dom gen`) |
166
+ | `missing-binding` at a template line | a hole or `data-if` name that state, props and derive do not have | add the field, or rename the hole |
167
+ | A list stays empty | `render` never assigns it, or guards it with a name that is not in `changed` | assign `this.lists.x.items` in `render`, guarded by the name of what it shows |
168
+ | A child component does not update | an object prop was changed in place | pass a new object |
169
+ | No types in a parent's `refs.child` | the child class is not exported, or is defined outside the scanned folders | `export class`, and keep it in a folder `ts-dom gen` scans |
170
+
171
+ ## See also
172
+
173
+ - Templates: https://github.com/Pixmi/nativele/blob/main/docs/templates.md
174
+ - Components and data flow: https://github.com/Pixmi/nativele/blob/main/docs/components.md
175
+ - Lists: https://github.com/Pixmi/nativele/blob/main/docs/lists.md
176
+ - Recipes (loading states, dialogs, forms, store data): https://github.com/Pixmi/nativele/blob/main/docs/recipes.md
177
+ - Setup, checks and lint: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: nativele-integration
3
+ description: "Use Nativele web components inside an app built with a UI framework that has no Nativele Skill of its own (Preact, Solid, Lit, ...; React, Vue, Svelte and Angular have their own): defining before render, props as properties, listening to events, children and slots, lifecycle, and template types. Use when a project depends on @nativele/ts-dom and on such a framework."
4
+ metadata:
5
+ tier: experimental
6
+ nativele: ">=0.5.1 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "vue@3.5.44 vite@8.3.6 @vitejs/plugin-vue@6.0.10 vue-tsc@3.3.14 typescript@6.0.3"
9
+ author: "Nativele maintainers"
10
+ detects: "preact solid-js"
11
+ ---
12
+
13
+ # Nativele in a framework app
14
+
15
+ ## When to use
16
+
17
+ - The app is built with a UI framework and wants to use components written with Nativele (`@nativele/ts-dom`), and no Skill exists for that framework yet. React, Vue, Svelte and Angular have their own (`nativele-react`, `nativele-vue`, `nativele-svelte`, `nativele-angular`); `npx ts-dom skills` lists every Skill.
18
+ - It is a procedure for finding out how the framework treats custom elements and connecting it correctly. It does not cover setting the framework itself up: start from the app as it is, with its own type checker and build.
19
+ - A framework with its own file format type-checks it with its own tool (Vue: `vue-tsc`). `ts-dom check` runs `vue-tsc` instead of `tsc` when the project's `package.json` lists it; for other formats see Troubleshooting.
20
+ - Not for writing the Nativele components themselves: use `nativele-core` (`npx ts-dom skills add nativele-core`).
21
+
22
+ Nativele components are native custom elements. The framework renders their tags like any other tag; nothing wraps them. Every rule below follows from that.
23
+
24
+ ## Versions
25
+
26
+ | Tool | Verified version |
27
+ |---|---|
28
+ | @nativele/ts-dom | 0.5.1 |
29
+ | vue | 3.5.44 |
30
+ | vite | 8.3.6 |
31
+ | @vitejs/plugin-vue | 6.0.10 |
32
+ | vue-tsc | 3.3.14 |
33
+ | typescript | 6.0.3 |
34
+
35
+ Verified on 2026-11-05 by an agent that followed this Skill in a fresh Vue app made with `create-vite` (`vue-ts`), and checked in Google Chrome. The rules come from the custom element standard and hold for any framework, but each framework's own behavior (step 3) is verified only for the frameworks named here and in `nativele-react`.
36
+
37
+ ## Set-up
38
+
39
+ 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts`, written with the core Skill.
40
+ 2. Connect the build: with Vite, add `tsDom({ dirs: ["src/nv"] })` from `@nativele/ts-dom/vite` to `plugins`, and pass the same folder to every `ts-dom` command (`npx ts-dom check src/nv`). In the tsconfig whose `include` covers that folder, set `"strict": true`. If the project lints with oxlint, add `{ "name": "ts-dom", "specifier": "@nativele/ts-dom/oxlint" }` to `jsPlugins` and `"ts-dom/unused-markup": "error"` to `rules`. Add `*.html.ts` and `ts-dom-tags.ts` to `.gitignore`. Run `npx ts-dom doctor src/nv` and fix what it reports.
41
+ 3. Find out, from the framework's documentation on custom elements (often called "web components"), these four things before writing any template:
42
+
43
+ | Question | Why it matters |
44
+ |---|---|
45
+ | Does the compiler need to be told that hyphenated tags are custom elements? | Otherwise it may try to resolve them as framework components and warn or fail. |
46
+ | Does a binding set a JS property or an HTML attribute on a custom element, and how do you force a property? | Objects and arrays reach a Nativele component only as properties. |
47
+ | How do you listen to a DOM event with an exact, hyphenated name? | Nativele events use names like `item-select`, and some syntaxes change the case. |
48
+ | Does the framework add, move and remove the children it renders inside the tag? | Light DOM slots move those children (limitation 3). |
49
+
50
+ 4. Import every Nativele component module in the app's entry file, before the framework mounts anything:
51
+
52
+ ```ts
53
+ // src/main.ts
54
+ import "./nv/info-card.js";
55
+ // then create and mount the app
56
+ ```
57
+
58
+ Many frameworks decide between property and attribute by checking whether the element already has the property, which is only true after `define()` has run.
59
+
60
+ 5. Give the framework's template type checker the Nativele tags. `ts-dom gen` writes `ts-dom-tags.ts`, which adds every exported component to `HTMLElementTagNameMap`; a component's props are the keys of its `props` member, and its events are in the optional `"~emits"` member (event name to `detail` type). Build the framework's tag declaration from these, so new components need no edit; `nativele-react` has the TSX one and `nativele-vue` the Vue one. Check how the framework names an event listener in its types: Vue types `@user-select` as the prop `onUserSelect`. If the framework has no typed templates, skip this step.
61
+
62
+ ## Example
63
+
64
+ What every framework does underneath, in plain DOM calls. Check the framework's output against it:
65
+
66
+ ```ts
67
+ import "./nv/info-card.js"; // define() runs first
68
+
69
+ const card = document.createElement("info-card");
70
+ card.label = "Ada"; // a string prop: property or attribute label="Ada"
71
+ card.item = { id: 1, name: "Ada" }; // an object prop: property only
72
+ card.addEventListener("item-select", (e) => { // the exact event name, on the element itself
73
+ console.log(e.detail.id);
74
+ });
75
+ host.append(card); // connected() runs here
76
+ card.item = { id: 2, name: "Grace" }; // a new object re-renders; changing item.name in place does not
77
+ card.remove(); // disconnected() runs, bind() subscriptions end
78
+ ```
79
+
80
+ In a framework, the same calls come from its template syntax. `nativele-vue` (`npx ts-dom skills nativele-vue`) is a complete worked example: its compiler option, property binding, event listener and template type declaration answer the four questions of step 3 for Vue.
81
+
82
+ ## Known limitations
83
+
84
+ 1. **Define before render.** Import the component modules before the framework mounts (step 4). A framework that sees an undefined element may write an attribute instead of a property, and an object then arrives as the text `"[object Object]"`. A property set before `define()` is kept: Nativele takes it over when the element is upgraded.
85
+ 2. **Props.** A prop whose default is a string, number or boolean can also come from an attribute, with camelCase names in kebab-case (`maxLength` from `max-length`, a boolean `true` when the attribute is present). Arrays and objects can only be set as properties. Pass a new object when data changes: props are compared by reference.
86
+ 3. **Light DOM slots and framework-managed children do not mix.** In the default Light DOM mode (`Component(tpl)` without `shadow`), Nativele moves the tag's children into its slot positions; a framework that later inserts, removes or reorders those children can fail or leave nodes outside the slot. Use one of:
87
+ - `shadow: true` on the component, so the browser assigns slots natively and the children can change freely;
88
+ - one stable element per slot that the framework never removes, with the changing content inside it (`<div slot="title">...</div>`, `<div>...</div>` for the default slot). A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
89
+ 4. **Events do not bubble.** `emit` dispatches a `CustomEvent` on the component that does not bubble, so the listener must be on that element, not on an ancestor. The event data is `event.detail`.
90
+ 5. **Lifecycle.** `connected()` runs each time the element is inserted and `disconnected()` each time it is removed; a framework that moves an element (reordering a keyed list) runs both. `bind` subscriptions end on removal and start again on insertion, and the children already in a Light DOM component's slots move with it.
91
+ 6. **The framework's templates are checked by the framework's type checker**, with the declaration from step 5, not by the Nativele rules. How much it checks depends on the framework: `vue-tsc` reports a wrong prop type or event detail, but not an unknown prop name.
92
+
93
+ ## Verify
94
+
95
+ ```sh
96
+ npx ts-dom check src/nv # expect: ts-dom check: no issues
97
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
98
+ ```
99
+
100
+ Then run the app's own type check and build (for example `npm run build` when it runs the framework's type checker). Keep one wrong prop per component kind that has props in a file the app never imports, under the framework's expect-error comment (a template comment such as `<!-- @ts-expect-error -->` is often not honored; check that a wrong prop fails without it), so the declaration is proved to type props.
101
+
102
+ In the browser, render a component with an object prop and an event listener, and check:
103
+
104
+ - the element has no attribute for the object prop (`el.getAttributeNames()`) and `el.<prop>` is the object;
105
+ - the listener runs once per event;
106
+ - children that the framework toggles, reorders, adds and removes stay in the right slot, with no errors or framework warnings in the console (run this check once against the dev server too: production builds drop framework warnings). Read a slot with `el.assignedNodes(name)` (`""` is the default slot), which works in both modes; a Light DOM component has no `<slot>` elements once rendered. Frameworks leave comment and empty text nodes as anchors among the children, so compare elements, not all nodes.
107
+
108
+ ## Troubleshooting
109
+
110
+ | Symptom | Cause | Fix |
111
+ |---|---|---|
112
+ | An object prop shows `[object Object]`, or the element has an attribute holding it | the framework set an attribute: the element was not defined yet, or the binding syntax sets attributes | import the module before mounting; use the framework's property binding |
113
+ | The framework warns that it cannot resolve the component `info-card` | its compiler treats the tag as a framework component | mark hyphenated tags as custom elements in the framework's compiler options |
114
+ | The event listener never runs | the name was changed (camelCase), or the listener is on an ancestor | listen with the exact name, on the element that emits |
115
+ | Children vanish, appear outside the slot, or the framework throws during an update | framework-managed children inside a Light DOM component with slots | `shadow: true`, or a stable element per slot |
116
+ | `ts-dom check` reports `TS2307: Cannot find module` for a framework file (`./App.vue`, ...) | plain `tsc` cannot read the framework's files | Vue: keep `vue-tsc` in `package.json` (Vue's own type checker; `ts-dom check` uses it when listed). Other formats: pass `--project=` a tsconfig whose `include` covers only the Nativele folder, and type-check the rest with the framework's tool |
117
+ | The framework's type checker does not know the tag | no declaration built from `ts-dom-tags.ts`, or the component class is not exported | add the declaration (step 5), export the class, run `ts-dom gen` |
118
+
119
+ ## See also
120
+
121
+ - Props, events and data flow: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-flow-props-down-events-up
122
+ - Slots, Light DOM and Shadow DOM: https://github.com/Pixmi/nativele/blob/main/docs/components.md#light-default-and-shadow
123
+ - Lifecycle: https://github.com/Pixmi/nativele/blob/main/docs/components.md#components
124
+ - Setup and checks: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md
125
+ - A complete framework integration: `npx ts-dom skills nativele-react`
@@ -0,0 +1,213 @@
1
+ ---
2
+ name: nativele-react
3
+ description: "Use Nativele web components inside a React 19 app: passing props, receiving events, children and slots, lifecycle, and TSX types. Use when a project depends on react and @nativele/ts-dom."
4
+ metadata:
5
+ tier: official
6
+ nativele: ">=0.5.1 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "react@19.3.1 react-dom@19.3.1 @types/react@19.3.0 @types/react-dom@19.3.0 vite@8.3.5 @vitejs/plugin-react@6.2.1 typescript@7.0.2"
9
+ author: "Nativele maintainers"
10
+ detects: "react"
11
+ ---
12
+
13
+ # Nativele + React
14
+
15
+ ## When to use
16
+
17
+ - The project renders with React 19 and wants to use components written with Nativele (`@nativele/ts-dom`), for example a design-system layer or widgets shared with non-React pages.
18
+ - Signals: `react` and `@nativele/ts-dom` in `package.json`, `.tsx` files that render tags with a hyphen (`<info-card>`).
19
+ - Not for React 18 or earlier: React 18 sets every prop on a custom element as an attribute and does not attach `on...` handlers to custom events. The steps below are verified only with React 19.
20
+
21
+ Nativele components are native custom elements. React renders them like any other tag; nothing wraps them.
22
+
23
+ ## Versions
24
+
25
+ | Tool | Verified version |
26
+ |---|---|
27
+ | @nativele/ts-dom | 0.5.1 |
28
+ | react, react-dom | 19.3.1 |
29
+ | @types/react, @types/react-dom | 19.3.0 |
30
+ | vite | 8.3.5 |
31
+ | @vitejs/plugin-react | 6.2.1 |
32
+ | typescript | 7.0.2 |
33
+
34
+ Verified on 2026-11-05 in Google Chrome 148 with the example project in the Nativele repository (`skills/nativele-react/example`), which CI runs again when the example or the library changes and once a month. With a different major version of React, treat every step as unverified.
35
+
36
+ ## Set-up
37
+
38
+ 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts`. Write them with the core Skill: `npx ts-dom skills add nativele-core`.
39
+ 2. Install: `npm i @nativele/ts-dom`.
40
+ 3. Set `"strict": true` in the tsconfig whose `include` covers that folder (`tsconfig.app.json` in Vite's React template, which leaves it off); Nativele's type checks depend on it. Other configs, such as the one for `vite.config.ts`, stay as they are.
41
+ 4. Add the Nativele Vite plugin next to the React plugin, pointing at that folder:
42
+
43
+ ```ts
44
+ // vite.config.ts
45
+ import { defineConfig } from "vite";
46
+ import react from "@vitejs/plugin-react";
47
+ import { tsDom } from "@nativele/ts-dom/vite";
48
+
49
+ export default defineConfig({ plugins: [tsDom({ dirs: ["src/nv"] }), react()] });
50
+ ```
51
+
52
+ Pass the same folder to every `ts-dom` command (`npx ts-dom check src/nv`), so there is one `ts-dom-tags.ts`, in `src/nv`.
53
+
54
+ 5. If the project lints with oxlint (`.oxlintrc.json`, as in Vite's React template), load the ts-dom rules there: add `{ "name": "ts-dom", "specifier": "@nativele/ts-dom/oxlint" }` to `jsPlugins` and `"ts-dom/unused-markup": "error"` to `rules`.
55
+
56
+ 6. Import every Nativele component module in the entry file, before React renders anything:
57
+
58
+ ```tsx
59
+ // src/main.tsx
60
+ import "./nv/info-card.js";
61
+ import "./nv/shadow-card.js";
62
+ // then createRoot(...).render(...)
63
+ ```
64
+
65
+ React 19 sets a value as a property only when the element is already defined; before that it writes an attribute, and an object prop becomes the text `"[object Object]"`.
66
+
67
+ 7. Add the TSX declaration below as `src/nativele-react.d.ts`. It reads the tag map that `ts-dom gen` writes (`ts-dom-tags.ts`), so every exported component gets typed props and events, and new components need no edit:
68
+
69
+ ```ts
70
+ // src/nativele-react.d.ts
71
+ import type { DetailedHTMLProps, HTMLAttributes } from "react";
72
+
73
+ type NativeleTag = Extract<keyof HTMLElementTagNameMap, `${string}-${string}`>;
74
+ type NativeleProps<E> = E extends { readonly props: infer P } ? { [K in keyof P & keyof E]?: E[K] } : unknown;
75
+ type NativeleEvents<E> = E extends { readonly "~emits"?: infer M }
76
+ ? { [K in keyof NonNullable<M> & string as `on${K}`]?: (event: CustomEvent<NonNullable<M>[K]>) => void }
77
+ : unknown;
78
+ type NativeleElementProps<E extends HTMLElement> = Omit<DetailedHTMLProps<HTMLAttributes<E>, E>, keyof NativeleProps<E>> &
79
+ NativeleProps<E> &
80
+ NativeleEvents<E>;
81
+ type NativeleIntrinsics = { [K in NativeleTag]: NativeleElementProps<HTMLElementTagNameMap[K]> };
82
+
83
+ declare module "react" {
84
+ namespace JSX {
85
+ interface IntrinsicElements extends NativeleIntrinsics {}
86
+ }
87
+ }
88
+ ```
89
+
90
+ The component class must be exported and passed to `define()` in a folder that `ts-dom gen` scans, so it appears in `ts-dom-tags.ts`.
91
+
92
+ The generated files are not committed, so on a clean checkout the type check in `npm run build` runs before anything has written them and fails with `TS2307: Cannot find module './x.html.js'`. Generate them first: `"build": "ts-dom gen src/nv && tsc -b && vite build"` (the template's script with `ts-dom gen src/nv &&` in front).
93
+
94
+ ## Example
95
+
96
+ A Nativele component with props and an event:
97
+
98
+ ```html
99
+ <!-- src/nv/info-card.html -->
100
+ <span>{{ label }}</span>
101
+ <b>{{ count }}</b>
102
+ <i data-ref="itemName"></i>
103
+ <button type="button" data-on-click="pick">pick</button>
104
+ ```
105
+
106
+ ```ts
107
+ // src/nv/info-card.ts
108
+ import { Component, define, event } from "@nativele/ts-dom";
109
+ import tpl from "./info-card.html.js";
110
+
111
+ export interface Item {
112
+ id: number;
113
+ name: string;
114
+ }
115
+
116
+ export class InfoCard extends Component(tpl, {
117
+ props: { label: "", count: 0, item: null as Item | null },
118
+ emits: { "item-select": event<{ id: number }>() },
119
+ }) {
120
+ pick(): void {
121
+ const item = this.props.item;
122
+ if (item) this.emit("item-select", { id: item.id });
123
+ }
124
+ override render(view: { item: Item | null }): void {
125
+ this.refs.itemName.textContent = view.item ? view.item.name : "-";
126
+ }
127
+ }
128
+ define("info-card", InfoCard);
129
+ ```
130
+
131
+ Used from React:
132
+
133
+ ```tsx
134
+ <info-card label={label} count={count} item={item} onitem-select={(e) => select(e.detail.id)} />
135
+ ```
136
+
137
+ - `label`, `count` and `item` arrive as properties; the object is the same object React passed. Changing them from React state re-renders the Nativele component.
138
+ - The event handler prop is `on` followed by the exact event name: `onitem-select`. `e.detail` is typed from the component's `emits`.
139
+
140
+ A component with slots that receives React children: give it `shadow: true`, then pass children directly.
141
+
142
+ ```html
143
+ <!-- src/nv/shadow-card.html -->
144
+ <h2><slot name="title">Untitled</slot></h2>
145
+ <div><slot></slot></div>
146
+ ```
147
+
148
+ ```ts
149
+ // src/nv/shadow-card.ts
150
+ export class ShadowCard extends Component(tpl, { shadow: true }) {}
151
+ define("shadow-card", ShadowCard);
152
+ ```
153
+
154
+ ```tsx
155
+ <shadow-card>
156
+ {showTitle && <span slot="title">{title}</span>}
157
+ {rows.map((r) => <p key={r.id}>{r.text}</p>)}
158
+ </shadow-card>
159
+ ```
160
+
161
+ ## Known limitations
162
+
163
+ 1. **Define before render.** Import every Nativele module before React first renders its tag (step 6). Otherwise props become attributes and object props are lost.
164
+ 2. **Event prop names.** Only `on` plus the exact event name works (`onitem-select`); `onItemSelect` and `onItem-select` never fire. Nativele events do not bubble by default, so put the handler on the element that emits. A ref with `addEventListener` in `useEffect` also works (remove it in the cleanup).
165
+ 3. **Light DOM slots and React children do not mix.** In the default Light DOM mode (`Component(tpl)` with no `shadow`), Nativele moves the host's children into its slot positions. When React later inserts, removes or reorders those children it fails (`insertBefore`/`removeChild` errors that unmount the root) or leaves nodes outside the slot. Changing text in place and appending at the end happen to work, but do not rely on that. Use one of:
166
+ - `shadow: true` on the component (children can then change freely), or
167
+ - one stable React element per slot, and let React change only what is inside it:
168
+
169
+ ```tsx
170
+ <light-card>
171
+ <div slot="title">{showTitle && <span>{title}</span>}</div>
172
+ <div>{rows.map((r) => <p key={r.id}>{r.text}</p>)}</div>
173
+ </light-card>
174
+ ```
175
+
176
+ A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
177
+ 4. **New objects for changed props.** A Nativele component compares props by reference; pass a new object when the data changes (React state updates usually do).
178
+ 5. **TSX types need the declaration** in step 7; without it, every Nativele tag is `TS2339: Property 'info-card' does not exist on type 'JSX.IntrinsicElements'`. Check it with a `.tsx` that has a wrong prop under `// @ts-expect-error`, because `skipLibCheck` can hide mistakes inside the declaration itself. Keep these cases in a `.tsx` file of their own that the app never imports (the example uses `src/types-check.tsx`).
179
+ 6. Not verified: server-side rendering, form controls inside Nativele components submitting with a React form, and React versions other than 19.3.
180
+
181
+ React StrictMode is fine: it runs effects twice but does not insert the custom element twice, so `connected()` runs once, and `bind` subscriptions are released when React unmounts the element.
182
+
183
+ ## Verify
184
+
185
+ Run in the project:
186
+
187
+ ```sh
188
+ npx ts-dom check src/nv # expect: ts-dom check: no issues
189
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right
190
+ npx tsc --noEmit -p tsconfig.app.json # expect: no errors (keep one @ts-expect-error case per component kind)
191
+ npm run build # expect: ✓ built (it runs ts-dom gen, tsc -b and vite build)
192
+ ```
193
+
194
+ Pass `tsc` the tsconfig from step 3. A root `tsconfig.json` that only lists `references` (as in Vite's template) checks nothing itself, so `tsc --noEmit` without `-p` passes even with type errors.
195
+
196
+ In the browser, render a component with an object prop and an event handler, and check that the element has no `item` attribute (`el.getAttributeNames()`), that `el.item` is the object, and that the handler fires once per click. The example project's [`verify.mjs`](https://github.com/Pixmi/nativele/blob/main/skills/nativele-react/example/verify.mjs) does these checks with Playwright, plus children that React reorders, adds and removes inside a `shadow: true` component and inside stable wrappers in a Light DOM component. A Light DOM component has no `<slot>` elements once rendered; check where the children ended up with `el.assignedNodes(name)`, where `""` is the default slot.
197
+
198
+ ## Troubleshooting
199
+
200
+ | Symptom | Cause | Fix |
201
+ |---|---|---|
202
+ | `TS2307: Cannot find module './x.html.js'` from `npm run build` on a clean checkout | `tsc -b` runs before the generated files exist | start the script with `ts-dom gen src/nv &&` |
203
+ | An object prop shows `[object Object]` or is missing | the component was rendered before `define()` ran | import the component module in the entry file before rendering |
204
+ | The event handler never runs | the prop name is camelCased (`onItemSelect`) | use `on` plus the exact event name (`onitem-select`) |
205
+ | `insertBefore`/`removeChild` errors, the React root disappears | React children changed inside a Light DOM component with slots | use `shadow: true`, or stable wrapper elements per slot |
206
+ | `TS2339: Property 'x-y' does not exist on type 'JSX.IntrinsicElements'` | the declaration file is missing, or the component class is not exported | add `src/nativele-react.d.ts`, export the class, run `ts-dom gen` |
207
+ | `ts-dom check` reports `unused-ref` | a `data-ref` the class never reads | use it in the class or remove the attribute |
208
+
209
+ ## See also
210
+
211
+ - Components, props and events: https://github.com/Pixmi/nativele/blob/main/docs/components.md
212
+ - Slots, Light DOM and Shadow DOM: https://github.com/Pixmi/nativele/blob/main/docs/components.md#light-default-and-shadow
213
+ - Setup and checks: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md