@nativele/ts-dom 0.3.0 → 0.4.6
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.
- package/CHANGELOG.md +74 -0
- package/README.md +203 -17
- package/README.zh-TW.md +203 -17
- package/dist/{a11y-CeZcA10z.mjs → a11y-wWL9rie7.mjs} +175 -29
- package/dist/cli.mjs +1143 -22
- package/dist/index.d.mts +67 -8
- package/dist/index.mjs +48 -7
- package/dist/{lint-DS-IOjkf.mjs → lint-CP_i4VPA.mjs} +4 -3
- package/dist/oxlint-plugin.d.mts +12 -1
- package/dist/oxlint-plugin.mjs +59 -19
- package/dist/vite-plugin.d.mts +29 -6
- package/dist/vite-plugin.mjs +98 -19
- package/llms.txt +20 -2
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,80 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
10
10
|
- Adding a lint rule is a minor change; lint rules are warnings or opt-in and do not count as breaking.
|
|
11
11
|
- Before every release run `npm run verify:pack` (`publint` and `attw`), `tsc`, the unit tests, the browser tests, `npm run smoke` and `npm run smoke:no-bundler`; all of them run in `.github/workflows/ci.yml`.
|
|
12
12
|
|
|
13
|
+
## 0.4.6
|
|
14
|
+
|
|
15
|
+
Released together with 0.4.1 to 0.4.5, which were not published on their own.
|
|
16
|
+
|
|
17
|
+
- `ts-dom rules --explain=<id>` explains any rule id: why it exists (tied to the principle it protects), a wrong example and the fix. `ts-dom rules --json` prints every rule as data. Every rule id has an explanation; a test keeps them in step.
|
|
18
|
+
- The README's Checks section says which tool checks what: a formatter such as oxfmt for well-formed markup and layout (it reports misnested tags that the library's parser would silently repair), `ts-dom gen` for the template rules, `tsc` for names and types, lint for usage, data flow and accessibility, `ts-dom doctor` for the set-up, and `ts-dom check` for all of them at once.
|
|
19
|
+
|
|
20
|
+
## 0.4.5
|
|
21
|
+
|
|
22
|
+
Not published on its own; it ships with 0.4.6.
|
|
23
|
+
|
|
24
|
+
- Every rule id of `unused-markup`, `data-flow` and `a11y` is also a lint rule of its own (`ts-dom/unused-ref`, `ts-dom/missing-handler`, ...), so a project can enable rules one by one with their own severity. The three group rules stay.
|
|
25
|
+
- `missing-handler` offers a suggestion that adds the method to the component class.
|
|
26
|
+
- In the editor, the Oxc extension for VS Code (`oxlint --lsp`) shows the findings and offers the quick fix; `oxlint --fix-suggestions` applies it from the command line. Both are covered by tests that run the oxlint shipped with Vite+.
|
|
27
|
+
- Each template is analyzed once per file text and template text, however many rules ask for it.
|
|
28
|
+
|
|
29
|
+
## 0.4.4
|
|
30
|
+
|
|
31
|
+
Not published on its own; it ships with 0.4.6.
|
|
32
|
+
|
|
33
|
+
- `ts-dom doctor [dirs...]` checks the project set-up: a tsconfig with `strict`, the DOM lib, a module resolution other than `classic` and an `include` covering the template folders (read through `extends`: relative paths, packages and arrays; a solution-style tsconfig through its references); the runtime package being resolvable; templates imported as `./x.html` instead of `./x.html.js`; stale generated files; the Vite and oxlint plugins being configured; and a formatter (oxfmt, Prettier) set up while templates keep their whitespace. Page entries and `dist` are not counted as templates, and a folder that does not exist is reported. Stable rule ids, a `Fix:` per issue, `--json`, exit code 1 on errors.
|
|
34
|
+
|
|
35
|
+
## 0.4.3
|
|
36
|
+
|
|
37
|
+
Not published on its own; it ships with 0.4.6.
|
|
38
|
+
|
|
39
|
+
- `ts-dom check [dirs...]` runs everything in one pass for agents and CI: template diagnostics, generated files (regenerated, or reported as stale with `--no-write`), the project's own `tsc` (TypeScript 5, 6 or 7), and the lint rules (`--a11y` adds accessibility). TypeScript errors from the template contract point at the template line and column (`missing-binding`, `missing-handler`), item component problems get their own rule ids, and errors that only follow from a broken class (the ones naming that class) are left out. A tag defined twice is reported, and a tsc that does not run to completion (no tsconfig, a bad option) is reported as `tsc-failed` instead of passing silently. A solution-style tsconfig (`"files": []` with `references`) is checked through its referenced projects, a folder that does not exist is reported as `dir-not-found`, and every plain tsc error carries a `Fix:` hint. `--json` prints the issues as data; `--strict` fails on warnings.
|
|
40
|
+
- The README, the rules for AI agents and `llms.txt` use `ts-dom check` as the command to run after a change.
|
|
41
|
+
|
|
42
|
+
## 0.4.2
|
|
43
|
+
|
|
44
|
+
Not published on its own; it ships with 0.4.6.
|
|
45
|
+
|
|
46
|
+
- `ts-dom gen --check` writes nothing and lists generated files that are missing, outdated or orphaned (a deleted template) and the errors that would make `ts-dom gen` fail, with exit code 1 if there are any; `--json` prints them as data. `ts-dom gen` removes an orphaned generated module. A module generated with whitespace `collapse` records it, and a later `gen`, `gen --check`, `check` or `doctor` without the option keeps that mode (`--whitespace=preserve` switches back).
|
|
47
|
+
|
|
48
|
+
## 0.4.1
|
|
49
|
+
|
|
50
|
+
Not published on its own; it ships with 0.4.6.
|
|
51
|
+
|
|
52
|
+
- `npm run bench` measures lists of 1,000 to 10,000 items in real Chrome (create, update 10%, reverse, remove half, append) for component items, plain elements and a direct DOM baseline; the README's Performance section shows the results and when to choose which.
|
|
53
|
+
- Assigning `items` sets the prop of a new item before its element goes into the document, so the item component is connected with its item already set and renders once instead of first rendering without it. `replace()` and `upsert()` keep inserting first.
|
|
54
|
+
- README: a fourth example, a list of components (add, remove, sort, toggle, events up from the item), the one-line rule for choosing between `items =` and `replace()`, and how moving an item affects `connected()` and subscriptions with and without `moveBefore`. The introduction and the package description say what the library is built for: type safety and AI coding agents, which can correct their own mistakes from `tsc` errors and the `Fix:` hints of every diagnostic.
|
|
55
|
+
- `state`, `props`, the `view` passed to `render` and the argument of `derive` functions are typed `DeepReadonly` (class instances with private members keep their own type) (exported, with `PropAccessors`): changing an array or object inside them in place is a `tsc` error, since the list and child props compare by reference and would not update. `set()` accepts read-only values. A parent can still assign a child's props.
|
|
56
|
+
|
|
57
|
+
### Breaking changes
|
|
58
|
+
|
|
59
|
+
- Code that annotates `render`'s `view`, or a helper that takes the state, with mutable array types (`Item[]`) must use read-only ones (`readonly Item[]` or `DeepReadonly<State>`), and code that changes state objects in place no longer compiles.
|
|
60
|
+
|
|
61
|
+
## 0.4.0
|
|
62
|
+
|
|
63
|
+
### Lists of components
|
|
64
|
+
|
|
65
|
+
- `<template data-each="todos" data-key="id" data-prop="todo"><todo-row role="listitem"></todo-row></template>` and `this.lists.todos.items = todos`: each item is one component that receives its item as a property, matched by key. An element is kept for a key that stays (moved with `moveBefore`), created for a new key and removed for a key that is gone; reading `items` returns the items in document order.
|
|
66
|
+
- `tsc` checks the assigned items against the property type the item component declares and the `data-key` field (a string or a number). An item component defined in a scanned folder must be exported and listed in `ts-dom-tags.ts`, otherwise the assignment is a type error that names the cause; only components from outside the scanned folders get a loose type.
|
|
67
|
+
- New codegen rules `each-binding-incomplete`, `invalid-each-binding` and `each-binding-root`, and runtime errors with a `Fix:` hint for what types cannot see (`invalid-items`, `invalid-item`, `missing-key`, `duplicate-key`, `list-not-bound`, `unknown-prop`). A wrong `data-prop` throws before the list changes.
|
|
68
|
+
- The accessibility check `list-structure` accepts a custom element with `role="listitem"` as a list item.
|
|
69
|
+
- `replace()` stays for lists of plain elements, and `data-each` still cannot be nested: a list inside an item is that component's own list.
|
|
70
|
+
|
|
71
|
+
### Vite dev server
|
|
72
|
+
|
|
73
|
+
- Editing a template regenerates its `x.html.ts` and reloads the page once if a page uses it; a save that changes nothing does not reload. This works under Vite and Vite+ (which reports changed files by relative path).
|
|
74
|
+
- Deleting a template removes its generated module, and adding or removing a `define()` updates `ts-dom-tags.ts` and the child imports of the templates.
|
|
75
|
+
- A generated module rewritten by something else (`ts-dom gen --watch`, a checkout) reloads the page through Vite; duplicate change events and identical rewrites by other dev servers no longer reload it twice.
|
|
76
|
+
- `dirs` are resolved against Vite's `root` instead of the working directory. The `define()` scan is cached and redone only when a `.ts` file with a `define()` changes. Messages go to the logger of the server that saw the change.
|
|
77
|
+
|
|
78
|
+
### Fixes
|
|
79
|
+
|
|
80
|
+
- The accessibility messages say "natively" again where it had been replaced by "ts-domy".
|
|
81
|
+
|
|
82
|
+
### Breaking changes
|
|
83
|
+
|
|
84
|
+
- The Vite plugin's `dirs` are relative to Vite's `root`; a project whose `root` differs from the working directory must adjust them.
|
|
85
|
+
- A project whose item component class is not exported, or whose `ts-dom-tags.ts` is stale, now gets a type error on `items`.
|
|
86
|
+
|
|
13
87
|
## 0.3.0
|
|
14
88
|
|
|
15
89
|
- `ts-dom gen` rejects `<html>`, `<head>` and `<body>` inside a template (rule `document-tag`) instead of silently dropping them; a file that starts with `<!doctype html>` or `<html>` is still a page entry whatever its name.
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ A TypeScript-driven, HTML-first UI library.
|
|
|
4
4
|
|
|
5
5
|
Nativele publishes this library as `@nativele/ts-dom` (import it from `@nativele/ts-dom`); its command line tool is `ts-dom`.
|
|
6
6
|
|
|
7
|
-
A
|
|
7
|
+
A lightweight UI library: HTML describes the structure and where data goes, TypeScript owns behavior and state, and the native DOM renders it, with no virtual DOM and no template expressions. It is built for type safety and for working with AI agents: `tsc` checks every name the HTML uses, and every diagnostic says how to fix it. Complex UI is built by composing native custom elements.
|
|
8
|
+
|
|
9
|
+
**Nativele is made for AI coding agents.** An agent writes a template and a class, runs `ts-dom check`, and corrects its own mistakes from the type errors: a name the HTML uses that the class does not have, an event handler that does not exist, list items of the wrong shape. There is no template language to learn and no hidden reactivity to reason about, so what the agent reads in the HTML and the class is all that happens. Every diagnostic from `ts-dom gen` and `ts-dom lint` names its rule and ends with a `Fix:` hint the agent can apply, [`llms.txt`](llms.txt) describes the whole API on one page, and [Rules for AI Agents](#rules-for-ai-agents) is a list you can paste into a project's agent instructions.
|
|
8
10
|
|
|
9
11
|
English | [繁體中文](README.zh-TW.md)
|
|
10
12
|
|
|
@@ -31,15 +33,38 @@ define("user-card", UserCard);
|
|
|
31
33
|
|
|
32
34
|
If the HTML uses a name that `state` does not have, or `data-on-click` points to a method that does not exist, `tsc` reports an error.
|
|
33
35
|
|
|
36
|
+
## The Short Version
|
|
37
|
+
|
|
38
|
+
Six things cover most components:
|
|
39
|
+
|
|
40
|
+
1. **HTML only names things**: `{{ name }}` (text), `data-ref="x"` (an element you need), `data-if="x"` (shown or not), `<template data-each="x">` (a list), `data-on-click="method"` (an event), `<slot>`. There are no expressions; compute in TypeScript.
|
|
41
|
+
2. **Run `npx ts-dom gen` before `tsc`** whenever a `.html` changes. It writes the typed `x.html.ts` that `import tpl from "./x.html.js"` needs; until then `tsc` can only say the module is missing.
|
|
42
|
+
3. **Data lives in the class**: `state` (yours), `props` (the parent's), `derive` (computed). Change state only with `this.set()`; `{{ }}` and `data-if` with the same name update by themselves.
|
|
43
|
+
4. **Everything else goes in `render(view, changed)`**: lists, attributes, focus. A list of components is one assignment, `if (changed.has("todos")) this.lists.todos.items = view.todos`, with `data-key` and `data-prop` on its `<template data-each>`.
|
|
44
|
+
5. **Props go down, events go up**: the parent sets props; the child calls `this.emit("name", detail)` and the parent handles it with `data-on-name` on the child's tag.
|
|
45
|
+
6. **A handler is a public method** `(event, hit)`; inside a list item `hit.key` is the item's key.
|
|
46
|
+
|
|
47
|
+
## Rules for AI Agents
|
|
48
|
+
|
|
49
|
+
Paste these into your project's agent instructions (`AGENTS.md`, `CLAUDE.md` or similar):
|
|
50
|
+
|
|
51
|
+
- Write the `.html` and the `.ts` of a component together; never edit `x.html.ts` or `ts-dom-tags.ts`, which `ts-dom gen` writes.
|
|
52
|
+
- After changing any `.html` or component, run `npx ts-dom check` (add `--json` to read the result as data). It regenerates the generated files, runs `tsc`, maps the template contract errors back to the template line and column, and runs the lint rules. Fix every issue it reports, following its `Fix:` hint, instead of casting a type error away.
|
|
53
|
+
- Import templates as `./x.html.js`.
|
|
54
|
+
- No expressions in HTML: put computed values in `derive`, and change state only with `this.set()`.
|
|
55
|
+
- Every diagnostic from `ts-dom gen` and `ts-dom lint` ends with a `Fix:` hint; follow it. `npx ts-dom rules` lists all rule ids, and `npx ts-dom lint --a11y` also checks accessibility.
|
|
56
|
+
- A `data-each` list cannot be nested: put the inner list in its own `.html` as a child component.
|
|
57
|
+
- 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 = ...` with new objects for changed items (never mutate an assigned item), and let `tsc` check the items.
|
|
58
|
+
|
|
34
59
|
## Quick Start
|
|
35
60
|
|
|
36
61
|
```sh
|
|
37
62
|
npm i -D typescript && npm i @nativele/ts-dom
|
|
38
|
-
npx ts-dom gen # generate x.html.ts (fully typed) for every x.html
|
|
63
|
+
npx ts-dom gen # generate x.html.ts (fully typed) for every x.html; run it again after every .html change
|
|
39
64
|
npx tsc # type-check and compile; no Vite or other bundler needed
|
|
40
65
|
```
|
|
41
66
|
|
|
42
|
-
The
|
|
67
|
+
The four examples below are real, runnable files in `examples/readme/` (type-checked, linted, and tested).
|
|
43
68
|
|
|
44
69
|
### Example 1: Counter (state, derived values, events, data-if)
|
|
45
70
|
|
|
@@ -124,7 +149,7 @@ export class TodoList extends Component(tpl, {
|
|
|
124
149
|
}
|
|
125
150
|
|
|
126
151
|
// Automatic binding handles {{ remaining }}; the list is filled here.
|
|
127
|
-
override render(view: { items: Item[] }, changed: ReadonlySet<string>): void {
|
|
152
|
+
override render(view: { items: readonly Item[] }, changed: ReadonlySet<string>): void {
|
|
128
153
|
if (!changed.has("items")) return;
|
|
129
154
|
this.lists.items.replace(
|
|
130
155
|
view.items,
|
|
@@ -206,6 +231,97 @@ define("order-line", OrderLine);
|
|
|
206
231
|
|
|
207
232
|
The data flow goes in only one direction: the parent's state → the child's props (`this.refs.qty.value = ...`); the child's request → event → the parent's handler method.
|
|
208
233
|
|
|
234
|
+
### Example 4: A list of components (`items =`, props down, events up)
|
|
235
|
+
|
|
236
|
+
When an item has anything of its own (state, events, a nested list), make it a component and fill the list with one assignment. The item component receives its task as a prop and sends changes up as events:
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<!-- task-row.html -->
|
|
240
|
+
<label><input type="checkbox" data-ref="done" data-on-change="toggle" /> {{ title }}</label>
|
|
241
|
+
<button type="button" data-on-click="drop">Remove</button>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
// task-row.ts
|
|
246
|
+
import { Component, define, event } from "@nativele/ts-dom";
|
|
247
|
+
import tpl from "./task-row.html.js";
|
|
248
|
+
|
|
249
|
+
export interface Task {
|
|
250
|
+
id: number;
|
|
251
|
+
title: string;
|
|
252
|
+
done: boolean;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** One item of the list: it receives its task as a prop and asks the parent for changes through events. */
|
|
256
|
+
export class TaskRow extends Component(tpl, {
|
|
257
|
+
props: { task: null as Task | null },
|
|
258
|
+
derive: { title: (s) => s.task?.title ?? "" },
|
|
259
|
+
emits: { "task-toggle": event(), "task-remove": event() },
|
|
260
|
+
}) {
|
|
261
|
+
override render(view: { task: Task | null }): void {
|
|
262
|
+
const done = view.task?.done ?? false;
|
|
263
|
+
if (this.refs.done.checked !== done) this.refs.done.checked = done;
|
|
264
|
+
}
|
|
265
|
+
toggle(): void {
|
|
266
|
+
this.emit("task-toggle");
|
|
267
|
+
}
|
|
268
|
+
drop(): void {
|
|
269
|
+
this.emit("task-remove");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
define("task-row", TaskRow);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The parent names, on the `<template data-each>`, the field that identifies an item (`data-key`) and the prop that receives it (`data-prop`), and assigns the whole list in `render`:
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<!-- task-board.html -->
|
|
279
|
+
<form data-on-submit="add">
|
|
280
|
+
<input data-ref="title" aria-label="New task" />
|
|
281
|
+
<button type="submit">Add</button>
|
|
282
|
+
</form>
|
|
283
|
+
<button type="button" data-on-click="sortByTitle">Sort by title</button>
|
|
284
|
+
<ul>
|
|
285
|
+
<template data-each="tasks" data-key="id" data-prop="task">
|
|
286
|
+
<task-row role="listitem" data-on-task-toggle="toggleTask" data-on-task-remove="removeTask"></task-row>
|
|
287
|
+
</template>
|
|
288
|
+
</ul>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
// task-board.ts
|
|
293
|
+
import { Component, define } from "@nativele/ts-dom";
|
|
294
|
+
import type { EventHit } from "@nativele/ts-dom";
|
|
295
|
+
import type { Task } from "./task-row.js";
|
|
296
|
+
import tpl from "./task-board.html.js";
|
|
297
|
+
|
|
298
|
+
export class TaskBoard extends Component(tpl, { state: { tasks: [] as Task[], nextId: 1 } }) {
|
|
299
|
+
add(e: Event): void {
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
const title = this.refs.title.value.trim();
|
|
302
|
+
if (!title) return;
|
|
303
|
+
this.set((s) => ({ tasks: [...s.tasks, { id: s.nextId, title, done: false }], nextId: s.nextId + 1 }));
|
|
304
|
+
this.refs.title.value = "";
|
|
305
|
+
}
|
|
306
|
+
// A changed task is a new object: the list sets the prop only on items that are not the same object as before.
|
|
307
|
+
toggleTask(_e: Event, hit: EventHit): void {
|
|
308
|
+
this.set((s) => ({ tasks: s.tasks.map((t) => (t.id === hit.key ? { ...t, done: !t.done } : t)) }));
|
|
309
|
+
}
|
|
310
|
+
removeTask(_e: Event, hit: EventHit): void {
|
|
311
|
+
this.set((s) => ({ tasks: s.tasks.filter((t) => t.id !== hit.key) }));
|
|
312
|
+
}
|
|
313
|
+
sortByTitle(): void {
|
|
314
|
+
this.set((s) => ({ tasks: [...s.tasks].sort((a, b) => a.title.localeCompare(b.title)) }));
|
|
315
|
+
}
|
|
316
|
+
override render(view: { tasks: readonly Task[] }, changed: ReadonlySet<string>): void {
|
|
317
|
+
if (changed.has("tasks")) this.lists.tasks.items = view.tasks;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
define("task-board", TaskBoard);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Adding, removing, sorting and toggling all go through `this.set()`; the list keeps the element of each task that stays (so a checkbox keeps its focus), creates one for a new task, removes the ones that are gone, and sets `task` only on items that are a new object. `tsc` checks that the assigned items match the `task` prop of `<task-row>` and have an `id`.
|
|
324
|
+
|
|
209
325
|
## Design Principles
|
|
210
326
|
|
|
211
327
|
- HTML is the single source of structure.
|
|
@@ -264,6 +380,8 @@ Whitespace in text is kept as written. To collapse each run of whitespace into o
|
|
|
264
380
|
|
|
265
381
|
During development, use `npx ts-dom gen --watch` to keep watching `.html` files, together with `tsc --watch`.
|
|
266
382
|
|
|
383
|
+
`npx ts-dom gen --check` writes nothing: it lists generated files that are missing, outdated (their template or a `define()` changed) or orphaned (their template was deleted), and exits with code 1 if there are any (`--json` for machine-readable output). Use it in CI if your project commits the generated files, or to confirm they are fresh before reading type errors.
|
|
384
|
+
|
|
267
385
|
In your code, import templates with a `.js` suffix (standard NodeNext style; TS maps it to the generated `x.html.ts`):
|
|
268
386
|
|
|
269
387
|
```ts
|
|
@@ -297,6 +415,8 @@ export default defineConfig({
|
|
|
297
415
|
});
|
|
298
416
|
```
|
|
299
417
|
|
|
418
|
+
While the dev server runs, the plugin keeps the generated files in step: adding or editing a template regenerates its `x.html.ts` and reloads the page once if a page uses it (a save that changes nothing does not reload), deleting a template removes its `x.html.ts`, and adding or removing a `define()` updates `ts-dom-tags.ts` and the child imports of the templates. `dirs` are relative to Vite's `root`. The page reloads rather than hot-swapping the component because a custom element tag can be defined only once, so a new class cannot replace the running one.
|
|
419
|
+
|
|
300
420
|
When installing, we recommend installing `vite-plus` first and then `ts-dom` (when both are installed in the same npm run, `vite` may not be hoisted to the top level).
|
|
301
421
|
|
|
302
422
|
### Lint (optional)
|
|
@@ -314,6 +434,10 @@ lint: {
|
|
|
314
434
|
},
|
|
315
435
|
```
|
|
316
436
|
|
|
437
|
+
Each rule id is also a rule of its own, so a project can enable rules one by one and give each its own severity, for example `"ts-dom/unused-ref": "warn"` or `"ts-dom/missing-handler": "error"` instead of the whole `ts-dom/unused-markup` group. `ts-dom rules` lists the ids.
|
|
438
|
+
|
|
439
|
+
**In the editor.** Install the Oxc extension for VS Code (it runs `oxlint --lsp`) and keep the plugin in your oxlint config: the findings show on the template's import line as you type, and `missing-handler` offers a quick fix that adds the method to the component class. From the command line, `oxlint --fix-suggestions` applies the same fix. A fix can only change the file being linted, so problems that live in the HTML (an unused `data-ref`, broken markup) are reported but not fixed there; for markup structure and layout use a formatter such as oxfmt.
|
|
440
|
+
|
|
317
441
|
Without oxlint, the same checks are available through the CLI:
|
|
318
442
|
|
|
319
443
|
```sh
|
|
@@ -322,6 +446,8 @@ npx ts-dom lint --a11y # additionally check ARIA / accessibility (warnings)
|
|
|
322
446
|
npx ts-dom lint --strict # warnings also make the exit code 1 (errors always do)
|
|
323
447
|
npx ts-dom lint --json # output JSON: { "issues": [{ file, line, col, severity, rule, message }] }
|
|
324
448
|
npx ts-dom rules # list all rule ids and descriptions
|
|
449
|
+
npx ts-dom rules --explain=unused-ref # why the rule exists, a wrong example and the fix
|
|
450
|
+
npx ts-dom rules --json # every rule as data: { id, group, description, why, bad, good }
|
|
325
451
|
```
|
|
326
452
|
|
|
327
453
|
Every message has the same format. It explains **what is wrong, why, and how to fix it**, and includes the line and column in the HTML:
|
|
@@ -333,7 +459,7 @@ a.html:3:1 warning [unknown-prop] <x-kid lable> is not a declared prop of <x-kid
|
|
|
333
459
|
|
|
334
460
|
Exit codes: 0 = no problems, 1 = there are errors (or warnings when `--strict` is used). Messages from the oxlint plugin start with `./x.html.js:line:column [rule id] message` and are reported on the `import` line.
|
|
335
461
|
|
|
336
|
-
Recommended check workflow: `ts-dom gen
|
|
462
|
+
Recommended check workflow: `ts-dom check` (it runs `gen`, `tsc` and `lint` in one pass; see "Checks").
|
|
337
463
|
|
|
338
464
|
## How Types Are Generated
|
|
339
465
|
|
|
@@ -486,6 +612,8 @@ Design trade-offs:
|
|
|
486
612
|
- `set` may be called inside `render`; it is processed in another round after the current one ends. If it has not stabilized after 50 consecutive rounds, a "suspected infinite loop" error is thrown.
|
|
487
613
|
- When `state` is not used, the original `refs`, `text`, `show`, `bind`, and `createStore` work as before, fully backward compatible.
|
|
488
614
|
|
|
615
|
+
**Read-only all the way down**: `state`, `props`, the `view` passed to `render` and the argument of each `derive` function are typed `DeepReadonly`: arrays and objects inside them cannot be changed in place. `this.state.items[0].done = true` or `this.state.items.push(x)` is a `tsc` error, because an object changed in place keeps its identity and the list and child props, which compare by reference, would not update. Pass new objects instead: `this.set((s) => ({ items: s.items.map((i) => (i.id === id ? { ...i, done: true } : i)) }))`. When you annotate `render`'s `view` or a helper that takes the state, use read-only types (`readonly Item[]`, or `DeepReadonly<State>` from `@nativele/ts-dom`).
|
|
616
|
+
|
|
489
617
|
## Events: data-on-*
|
|
490
618
|
|
|
491
619
|
HTML writes only the event name and the method name; the logic is in TS:
|
|
@@ -621,15 +749,24 @@ The example [examples/signup](examples/signup/signup-form.ts) does exactly this,
|
|
|
621
749
|
|
|
622
750
|
## Performance
|
|
623
751
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
|
627
|
-
|
|
628
|
-
| 1,000 |
|
|
629
|
-
| 5,000 |
|
|
630
|
-
| 10,000 |
|
|
631
|
-
|
|
632
|
-
|
|
752
|
+
`npm run bench` runs a large list benchmark in real Chrome and prints this table. Each item shows one text; the times are the median of 5 runs and count script execution only (no layout or paint). They were measured on one machine (Chrome on WSL), so read them as proportions, not absolute numbers. "Direct DOM" is a hand-written keyed list with plain DOM calls, the floor any library adds to.
|
|
753
|
+
|
|
754
|
+
| Approach | Items | Create | Update 10% | Reverse | Remove half | Append 1,000 |
|
|
755
|
+
|---|---|---|---|---|---|---|
|
|
756
|
+
| Components (`items =`) | 1,000 | 15.4 ms | 0.5 ms | 1.8 ms | 1.1 ms | 14.6 ms |
|
|
757
|
+
| Components (`items =`) | 5,000 | 80.5 ms | 1.6 ms | 10 ms | 5.4 ms | 17.6 ms |
|
|
758
|
+
| Components (`items =`) | 10,000 | 160.6 ms | 3.2 ms | 21 ms | 11.3 ms | 16 ms |
|
|
759
|
+
| Plain elements (`replace()`) | 1,000 | 2.8 ms | 0.1 ms | 0.7 ms | 0.2 ms | 2.9 ms |
|
|
760
|
+
| Plain elements (`replace()`) | 5,000 | 15.8 ms | 0.7 ms | 4.3 ms | 1.7 ms | 3.7 ms |
|
|
761
|
+
| Plain elements (`replace()`) | 10,000 | 42 ms | 1.4 ms | 8.6 ms | 3.3 ms | 4.4 ms |
|
|
762
|
+
| Direct DOM (baseline) | 1,000 | 0.8 ms | 0.2 ms | 1 ms | 0.3 ms | 0.8 ms |
|
|
763
|
+
| Direct DOM (baseline) | 5,000 | 4 ms | 0.8 ms | 4.3 ms | 1.6 ms | 1.1 ms |
|
|
764
|
+
| Direct DOM (baseline) | 10,000 | 8.7 ms | 1.7 ms | 10.9 ms | 3.9 ms | 1.6 ms |
|
|
765
|
+
|
|
766
|
+
- Creating component items costs about 15 µs per item, because each item is a custom element with its own template, refs and lifecycle; plain elements cost about 3–4 µs and direct DOM about 1 µs.
|
|
767
|
+
- Once the items exist, updating, reordering and removing them stay within about two times of direct DOM: only items that are a new object are touched, and kept elements are moved, not rebuilt.
|
|
768
|
+
- A frame has about 16 ms. A list of a few thousand component items is comfortable; for tens of thousands, use plain elements with `replace()`, show a page or a window of the list at a time, or keep component items for the rows that need state and events.
|
|
769
|
+
- Event delegation adds no listeners as the list grows.
|
|
633
770
|
|
|
634
771
|
## Examples
|
|
635
772
|
|
|
@@ -670,6 +807,33 @@ There is no host element, so `data-on-*` attributes do nothing here (listen on `
|
|
|
670
807
|
|
|
671
808
|
## Lists
|
|
672
809
|
|
|
810
|
+
There are two ways to fill a list, and one rule decides between them: **an item that has anything of its own (state, events, a nested list) is a component, filled with `items =`; an item that is a few texts and `data-if` is plain elements, filled with `replace()`.** Example 4 shows the first, Example 2 the second.
|
|
811
|
+
|
|
812
|
+
### Items as components: `items = ...`
|
|
813
|
+
|
|
814
|
+
Name the field that identifies an item and the property that receives it, on the `<template data-each>`; the item is one custom element:
|
|
815
|
+
|
|
816
|
+
```html
|
|
817
|
+
<ul>
|
|
818
|
+
<template data-each="todos" data-key="id" data-prop="todo">
|
|
819
|
+
<todo-row role="listitem" data-on-toggled="toggle"></todo-row>
|
|
820
|
+
</template>
|
|
821
|
+
</ul>
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
```ts
|
|
825
|
+
render(view, changed) {
|
|
826
|
+
if (changed.has("todos")) this.lists.todos.items = view.todos;
|
|
827
|
+
}
|
|
828
|
+
toggle(_e: Event, hit: EventHit) { /* hit.key is the item's id */ }
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
Assigning `items` keeps the element of each key that stays (moved with `moveBefore` where the browser has it, so focus and state survive), creates one for a new key, removes the ones that are gone, and sets `todo` on each element whose item is not the same object as last time. Use immutable updates: changing a field of an object you already assigned and assigning the same object again does not update the item component, because props are compared by reference; assign a new object instead, for example `{ ...todo, done: true }`. The item component declares `todo` in its `props`, shows it however it likes, and tells the parent with `emit`; the parent reads `hit.key` in the handler. Reading `items` returns what was last assigned. `tsc` checks the assignment: each item must have the type of the `todo` property that `<todo-row>` declares and a `data-key` field (`id`), and the item component must declare the `data-prop` property; a list without `data-key` and `data-prop` takes no items. This relies on the component being registered in `ts-dom-tags.ts` (an exported `define("todo-row", TodoRow)` in a scanned directory). When the component is defined in a scanned directory but its class is not exported, or `ts-dom-tags.ts` does not list it yet, the assignment is a type error that says so (`itemComponentClassNotExported`, `itemComponentMissingFromTagMap`); only a component from outside the scanned directories accepts any item with the `id` field. At runtime, assigning throws when the value is not an array of objects, when an item has no string or number `id`, when two items share one, or when the template has no `data-key` and `data-prop`.
|
|
832
|
+
|
|
833
|
+
Inside a `<ul>` or `<ol>` the item component needs `role="listitem"`, because a custom element is not an `<li>`; the accessibility check (`list-structure`) asks for it. `data-each` cannot be nested, on purpose: an item is one component, so a list inside an item is that component's own `data-each`. Every template then holds one level, with its own `refs`, `text` and event handling.
|
|
834
|
+
|
|
835
|
+
### Items as plain elements: `replace()`
|
|
836
|
+
|
|
673
837
|
```html
|
|
674
838
|
<ul>
|
|
675
839
|
<template data-each="tags">
|
|
@@ -706,9 +870,16 @@ Notes:
|
|
|
706
870
|
- `data-each` cannot be nested. Split it into two templates: put the inner list in its own `.html` (a child component with its own `data-each`) and use that component's tag inside the outer item.
|
|
707
871
|
- `<slot>` cannot be placed inside an item.
|
|
708
872
|
|
|
873
|
+
### Moving items, connecting and subscriptions
|
|
874
|
+
|
|
875
|
+
- In a browser with `moveBefore`, a moved item stays connected: neither `disconnected()` nor `connected()` runs, and its focus, scroll position and subscriptions are untouched.
|
|
876
|
+
- In a browser without it, a moved item is taken out and put back: `disconnected()` runs and releases everything registered with `bind`, `on` and `onCleanup`, then `connected()` runs again. Its element, props and state stay the same.
|
|
877
|
+
- So subscribe in `connected()` (`this.bind(store, ...)`, `this.on(...)`), not in the constructor or a field initializer: then the subscription is made again after a move in every browser. A subscription made in the constructor is released by the first move and never comes back.
|
|
878
|
+
- Data that every item needs (a theme, the current user, a registry) belongs in a `createStore` that the item component binds in `connected()`, not copied into every item object.
|
|
879
|
+
|
|
709
880
|
### Order of list updates
|
|
710
881
|
|
|
711
|
-
`replace()` and `upsert()` put a new item's element into the document first and then call `update`, so a child component placed in an item is connected before the parent has set its properties
|
|
882
|
+
Assigning `items` sets the `data-prop` property of a new item before its element goes into the document, so the item component is connected with its item already set and its first render shows it. `replace()` and `upsert()` put a new item's element into the document first and then call `update`, so a child component placed in such an item is connected before the parent has set its properties; there, build per-item state when the child first receives data (for example in a property setter), and guard a render in `connected()` with the data it needs.
|
|
712
883
|
|
|
713
884
|
## Trusted Types, Translations and Icons
|
|
714
885
|
|
|
@@ -724,9 +895,24 @@ Notes:
|
|
|
724
895
|
|
|
725
896
|
## Checks
|
|
726
897
|
|
|
898
|
+
Each tool checks what it can see, and none repeats another:
|
|
899
|
+
|
|
900
|
+
| Tool | What it checks |
|
|
901
|
+
|---|---|
|
|
902
|
+
| A formatter such as oxfmt (`oxfmt --check`, or `vp fmt --check`) | Whether the template markup is well formed (a misnested closing tag such as `<p><div></p></div>` is a syntax error there, while the HTML parser the library uses would silently repair it into a different tree) and laid out consistently. It knows nothing of `{{ }}` or `data-*` names. Generate with whitespace `collapse` when you use one, or reformatting can change the spacing on the page. |
|
|
903
|
+
| `ts-dom gen` | The template rules: holes, `data-each`, `data-if`, `data-on-*`, forbidden markup. |
|
|
904
|
+
| `tsc` | That every name in the HTML exists in the component with the right type, and that lists, props and events fit. |
|
|
905
|
+
| `ts-dom lint` / the oxlint plugin | What the HTML declares but the class never uses, data flow between components, accessibility. |
|
|
906
|
+
| `ts-dom doctor` | The project set-up that the other tools depend on. |
|
|
907
|
+
| `ts-dom check` | Runs `gen`, `tsc` and `lint` in one pass and points the template contract errors at the template. |
|
|
908
|
+
|
|
727
909
|
Four complementary checks. **Every message has a stable rule id and explains what is wrong, why, and how to fix it**; `npx ts-dom rules` lists all rules.
|
|
728
910
|
|
|
729
|
-
|
|
911
|
+
**When the set-up itself may be wrong: `npx ts-dom doctor [dirs...]`.** It checks what a project needs before any of the checks can work: a tsconfig with `strict`, the DOM lib, a module resolution that maps `./x.html.js` to the generated file and an `include` that covers the template folders; the runtime package being installed; templates imported as `./x.html.js` (not `./x.html`); generated files that are stale; the Vite plugin and the oxlint plugin being configured; and, when a formatter such as oxfmt or Prettier is set up, templates generated with whitespace `collapse`. Every issue has a stable rule id and a `Fix:` hint, and `--json` prints them as data.
|
|
912
|
+
|
|
913
|
+
**One command for all of them: `npx ts-dom check [dirs...]`.** It reports template diagnostics, regenerates the generated files (with `--no-write` it reports stale ones instead), runs the project's own `tsc` (TypeScript 5, 6 or 7; `--project=` picks the tsconfig), and runs the lint rules (`--a11y` adds the accessibility checks). TypeScript errors that come from the template contract point at the template, not at the `Component(...)` call: a `{{ title }}` with nothing named `title` is reported at its line and column in the `.html` as `missing-binding`, and a missing handler as `missing-handler`; item component problems get their own rule ids, and the errors that only follow from a broken class are left out. `--json` prints the issues as data, and the exit code is 1 when there is an error (with `--strict`, a warning too).
|
|
914
|
+
|
|
915
|
+
1. **codegen diagnostics** (`ts-dom gen` and the Vite plugin, with line and column numbers; on errors they abort and write no files): duplicate `data-ref`, `data-each` without a single root element, `data-each` not on a `<template>`, nested `data-each`, an incomplete or invalid `data-key` / `data-prop`, a `data-key` list whose item is not one custom element, `data-if` on an item root element, `<slot>` inside an item, `<style>` not at the first level, invalid holes, holes inside attributes, invalid `data-on-*` (including names that collide with DOM or ts-dom members), `<html>`, `<head>` and `<body>` inside a template, and markup that runs code or Trusted Types blocks: `<script>`, `on*` attributes, `srcdoc`, and `javascript:` URLs.
|
|
730
916
|
2. **tsc**: an error when TypeScript uses a name the HTML does not declare (`this.refs.nope`, `this.show("nope", true)`); all constraints between `state`, `props`, `derive`, `emits` and the HTML (see "Data Flow"); child component prop names and types; `define()` checks that the methods for `data-on-*` exist.
|
|
731
917
|
3. **Runtime**: `Component()` and components, at definition, connection, and use, throw errors that carry a `[rule id]` and a fix for the same problems.
|
|
732
918
|
4. **lint** (`ts-dom/unused-markup`, `ts-dom/data-flow`, `ts-dom/a11y`): markup declared in the HTML but never used (errors, with the HTML location), data flow suggestions and child component contracts (warnings), ARIA (warnings).
|
|
@@ -783,7 +969,7 @@ npx ts-dom lint --a11y [--strict]
|
|
|
783
969
|
| `heading-empty` | `h1`–`h6` must not be empty |
|
|
784
970
|
| `aria-role` | `role` must be a valid ARIA role |
|
|
785
971
|
| `aria-attr` | `aria-*` must be a valid ARIA attribute |
|
|
786
|
-
| `role-tabindex` | A non-
|
|
972
|
+
| `role-tabindex` | A non-natively-focusable element with an interactive role (button, tab, checkbox…) needs `tabindex` |
|
|
787
973
|
| `tabindex-positive` | Do not use a `tabindex` greater than 0 |
|
|
788
974
|
| `hidden-focusable` | A focusable element must not have `aria-hidden="true"` |
|
|
789
975
|
| `label-for` | `<label for>` must point to an `id` that exists in the template |
|