@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/README.zh-TW.md
CHANGED
|
@@ -6,7 +6,9 @@ Nativele 以 `@nativele/ts-dom` 發布這個函式庫(從 `@nativele/ts-dom`
|
|
|
6
6
|
|
|
7
7
|
[English](README.md) | 繁體中文
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
輕量的 UI 函式庫:HTML 描述結構與資料要放的位置,TypeScript 負責行為與狀態,由原生 DOM 渲染,沒有虛擬 DOM,也沒有範本表達式。它為型別安全與 AI agent 協作而設計:`tsc` 會檢查 HTML 用到的每個名稱,每則診斷都說明怎麼修。複雜的介面以原生自訂元素組合而成。
|
|
10
|
+
|
|
11
|
+
**Nativele 是為 AI coding agent 打造的。** agent 寫好範本與類別,執行 `ts-dom check`,就能從型別錯誤自行修正:HTML 用了類別沒有的名稱、事件處理方法不存在、清單項目的形狀不對。沒有要另外學的範本語言,也沒有要推敲的隱式響應式,agent 在 HTML 與類別裡讀到的就是實際發生的全部。`ts-dom gen` 與 `ts-dom lint` 的每則診斷都標明規則並附上可直接套用的 `Fix:` 提示,[`llms.txt`](llms.txt) 用一頁說明整個 API,[給 AI agent 的規則](#給-ai-agent-的規則) 則是可以直接貼進專案 agent 指引檔的清單。
|
|
10
12
|
|
|
11
13
|
```html
|
|
12
14
|
<!-- user-card.html:HTML 只描述結構、資料要放的位置、事件要找哪個方法 -->
|
|
@@ -31,15 +33,38 @@ define("user-card", UserCard);
|
|
|
31
33
|
|
|
32
34
|
HTML 用了 `state` 沒有的名稱、`data-on-click` 指向不存在的方法,`tsc` 都會報錯。
|
|
33
35
|
|
|
36
|
+
## 精簡版
|
|
37
|
+
|
|
38
|
+
六件事涵蓋大多數元件:
|
|
39
|
+
|
|
40
|
+
1. **HTML 只放名稱**:`{{ name }}`(文字)、`data-ref="x"`(需要用到的元素)、`data-if="x"`(顯示與否)、`<template data-each="x">`(清單)、`data-on-click="method"`(事件)、`<slot>`。沒有表達式,需要計算就在 TypeScript 做。
|
|
41
|
+
2. **`.html` 有改動就先跑 `npx ts-dom gen` 再跑 `tsc`**:它會產生 `import tpl from "./x.html.js"` 需要的型別檔 `x.html.ts`;在那之前 `tsc` 只會告訴你找不到模組。
|
|
42
|
+
3. **資料放在類別裡**:`state`(自己的)、`props`(父元件給的)、`derive`(算出來的)。state 只用 `this.set()` 改;同名的 `{{ }}` 與 `data-if` 會自己更新。
|
|
43
|
+
4. **其他都寫在 `render(view, changed)`**:清單、屬性、焦點。元件組成的清單只要一次指派,`if (changed.has("todos")) this.lists.todos.items = view.todos`,並在 `<template data-each>` 上寫 `data-key` 與 `data-prop`。
|
|
44
|
+
5. **props 往下、事件往上**:父元件設定 props;子元件呼叫 `this.emit("name", detail)`,父元件在子元件的標籤上用 `data-on-name` 處理。
|
|
45
|
+
6. **處理方法是公開方法** `(event, hit)`;在清單項目內 `hit.key` 就是該項目的 key。
|
|
46
|
+
|
|
47
|
+
## 給 AI agent 的規則
|
|
48
|
+
|
|
49
|
+
把這些貼進專案的 agent 指引檔(`AGENTS.md`、`CLAUDE.md` 等):
|
|
50
|
+
|
|
51
|
+
- 元件的 `.html` 與 `.ts` 一起寫;不要編輯 `x.html.ts` 與 `ts-dom-tags.ts`,它們是 `ts-dom gen` 產生的。
|
|
52
|
+
- 改了任何 `.html` 或元件之後,執行 `npx ts-dom check`(加 `--json` 以資料形式讀取結果)。它會重新產生產生檔、執行 `tsc`、把範本契約的錯誤對應回範本的行列,並跑 lint 規則。照每則的 `Fix:` 提示修好所有回報的問題,不要用型別斷言蓋掉型別錯誤。
|
|
53
|
+
- 範本一律以 `./x.html.js` 匯入。
|
|
54
|
+
- HTML 裡不放表達式:計算值放進 `derive`,state 只用 `this.set()` 改。
|
|
55
|
+
- `ts-dom gen` 與 `ts-dom lint` 的每則診斷結尾都有 `Fix:` 提示,照做。`npx ts-dom rules` 列出所有規則編號,`npx ts-dom lint --a11y` 另外檢查無障礙。
|
|
56
|
+
- `data-each` 清單不能巢狀:把內層清單放進自己的 `.html`,當成子元件。
|
|
57
|
+
- 元件組成的清單:在 `<template data-each>` 寫 `data-key` 與 `data-prop`,在 `<ul>` 或 `<ol>` 裡的項目元件加 `role="listitem"`,以 `this.lists.x.items = ...` 指派,變動的項目要給新物件(不要就地修改已指派的物件),並讓 `tsc` 檢查項目型別。
|
|
58
|
+
|
|
34
59
|
## 快速開始
|
|
35
60
|
|
|
36
61
|
```sh
|
|
37
62
|
npm i -D typescript && npm i @nativele/ts-dom
|
|
38
|
-
npx ts-dom gen # 為每個 x.html 產生 x.html.ts
|
|
63
|
+
npx ts-dom gen # 為每個 x.html 產生 x.html.ts(含型別);每次改了 .html 都要再跑一次
|
|
39
64
|
npx tsc # 型別檢查並編譯;不需要 Vite 或其他 bundler
|
|
40
65
|
```
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
下面四個範例都是 `examples/readme/` 裡真實可執行的檔案(有型別檢查、lint 與測試)。
|
|
43
68
|
|
|
44
69
|
### 範例 1:計數器(state、衍生值、事件、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
|
資料流只有一個方向:父層的 state → 子元件的 props(`this.refs.qty.value = ...`);子元件的請求 → 事件 → 父層的處理方法。
|
|
208
233
|
|
|
234
|
+
### 範例 4:元件組成的清單(`items =`、props 往下、事件往上)
|
|
235
|
+
|
|
236
|
+
項目有自己的東西(狀態、事件、內層清單)時,就把它做成元件,整份清單用一次指派填入。項目元件以 prop 接收自己的 task,用事件把變更往上送:
|
|
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
|
+
父元件在 `<template data-each>` 上寫出識別項目的欄位(`data-key`)與接收項目的 prop(`data-prop`),在 `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
|
+
新增、刪除、排序與切換都透過 `this.set()`;清單會保留還在的 task 的元素(所以核取方塊的焦點不會丟)、為新的 task 建立元素、移除消失的,並只對換成新物件的項目設定 `task`。`tsc` 會檢查指派的項目是否符合 `<task-row>` 的 `task` prop 並帶有 `id`。
|
|
324
|
+
|
|
209
325
|
## 設計原則
|
|
210
326
|
|
|
211
327
|
- HTML 是唯一的結構描述來源。
|
|
@@ -266,6 +382,8 @@ npx tsc # 型別檢查並編譯到 out/
|
|
|
266
382
|
|
|
267
383
|
開發時可用 `npx ts-dom gen --watch` 持續監看 `.html`,搭配 `tsc --watch`。
|
|
268
384
|
|
|
385
|
+
`npx ts-dom gen --check` 不寫任何檔案:它列出缺少、過期(範本或 `define()` 改過)或孤立(範本已刪除)的產生檔,有任何一個就以結束碼 1 結束(`--json` 輸出機器可讀格式)。專案若有提交產生檔,可以在 CI 使用;也可以在看型別錯誤之前,先確認產生檔是新的。
|
|
386
|
+
|
|
269
387
|
在程式中以 `.js` 結尾匯入範本(標準 NodeNext 寫法,TS 會對應到產生的 `x.html.ts`):
|
|
270
388
|
|
|
271
389
|
```ts
|
|
@@ -299,7 +417,9 @@ export default defineConfig({
|
|
|
299
417
|
});
|
|
300
418
|
```
|
|
301
419
|
|
|
302
|
-
|
|
420
|
+
dev server 執行期間,外掛會讓產生檔保持同步:新增或編輯範本會重新產生它的 `x.html.ts`,若有頁面用到就重新載入一次(存檔但內容沒變不會重載);刪除範本會移除它的 `x.html.ts`;新增或移除 `define()` 會更新 `ts-dom-tags.ts` 與範本的子元件匯入。`dirs` 相對於 Vite 的 `root`。這裡是重新載入而不是熱替換元件,因為同一個自訂元素標籤只能定義一次,新的類別無法取代正在執行的那個。
|
|
421
|
+
|
|
422
|
+
安裝時建議先裝 `vite-plus`,再裝 `@nativele/ts-dom`(npm 同一次安裝時可能不會把 `vite` 提升到頂層)。
|
|
303
423
|
|
|
304
424
|
### Lint(選用)
|
|
305
425
|
|
|
@@ -316,6 +436,10 @@ lint: {
|
|
|
316
436
|
},
|
|
317
437
|
```
|
|
318
438
|
|
|
439
|
+
每個規則代號也各自是一條規則,所以專案可以逐條啟用、各自設定嚴重程度,例如用 `"ts-dom/unused-ref": "warn"` 或 `"ts-dom/missing-handler": "error"`,而不是整組 `ts-dom/unused-markup`。`ts-dom rules` 會列出所有代號。
|
|
440
|
+
|
|
441
|
+
**在編輯器裡。** 在 VS Code 安裝 Oxc 擴充(它執行 `oxlint --lsp`),並在 oxlint 設定裡保留這個外掛:輸入時問題就會顯示在範本的 import 那一行,`missing-handler` 會提供快速修正,把方法加進元件類別。命令列可用 `oxlint --fix-suggestions` 套用同樣的修正。修正只能改動正在檢查的檔案,所以出在 HTML 裡的問題(沒用到的 `data-ref`、壞掉的標記)只會回報、不會在那裡修正;標記結構與排版請交給 oxfmt 這類格式化工具。
|
|
442
|
+
|
|
319
443
|
不用 oxlint 時,同樣的檢查可用 CLI:
|
|
320
444
|
|
|
321
445
|
```sh
|
|
@@ -324,6 +448,8 @@ npx ts-dom lint --a11y # 另外檢查 ARIA / 無障礙(警告)
|
|
|
324
448
|
npx ts-dom lint --strict # 警告也使結束碼為 1(錯誤一定會)
|
|
325
449
|
npx ts-dom lint --json # 輸出 JSON:{ "issues": [{ file, line, col, severity, rule, message }] }
|
|
326
450
|
npx ts-dom rules # 列出所有規則代號與說明
|
|
451
|
+
npx ts-dom rules --explain=unused-ref # 規則存在的理由、錯誤範例與修正
|
|
452
|
+
npx ts-dom rules --json # 以資料列出所有規則:{ id, group, description, why, bad, good }
|
|
327
453
|
```
|
|
328
454
|
|
|
329
455
|
每則訊息的格式都一樣,說明**哪裡錯、為什麼、怎麼修**,並附上 HTML 裡的行與欄:
|
|
@@ -335,7 +461,7 @@ a.html:3:1 warning [unknown-prop] <x-kid lable> is not a declared prop of <x-kid
|
|
|
335
461
|
|
|
336
462
|
結束碼:0 = 沒有問題,1 = 有錯誤(或使用 `--strict` 時有警告)。oxlint 外掛的訊息以 `./x.html.js:行:欄 [規則代號] 訊息` 開頭,回報在 `import` 那一行。
|
|
337
463
|
|
|
338
|
-
建議的檢查流程:`ts-dom gen
|
|
464
|
+
建議的檢查流程:`ts-dom check`(一次跑完 `gen`、`tsc` 與 `lint`,見「檢查」)。
|
|
339
465
|
|
|
340
466
|
## 型別如何產生
|
|
341
467
|
|
|
@@ -488,6 +614,8 @@ class Counter extends Component(tpl, {
|
|
|
488
614
|
- `render` 內可以呼叫 `set`,會在當次結束後再處理一輪;連續 50 輪還沒穩定會丟出「疑似無限迴圈」錯誤。
|
|
489
615
|
- 不使用 `state` 時,原本的 `refs`、`text`、`show`、`bind`、`createStore` 照常可用,完全向下相容。
|
|
490
616
|
|
|
617
|
+
**一路唯讀到底**:`state`、`props`、傳給 `render` 的 `view` 以及每個 `derive` 函式的參數,型別都是 `DeepReadonly`:裡面的陣列與物件都不能就地修改。`this.state.items[0].done = true` 或 `this.state.items.push(x)` 都是 `tsc` 錯誤,因為就地修改的物件保有原本的參考,以參考比較的清單與子元件 props 不會更新。請改成傳入新物件:`this.set((s) => ({ items: s.items.map((i) => (i.id === id ? { ...i, done: true } : i)) }))`。替 `render` 的 `view` 或接收 state 的輔助函式標註型別時,請用唯讀型別(`readonly Item[]`,或從 `@nativele/ts-dom` 匯入的 `DeepReadonly<State>`)。
|
|
618
|
+
|
|
491
619
|
## 事件:data-on-*
|
|
492
620
|
|
|
493
621
|
HTML 只寫事件名稱與方法名稱,邏輯在 TS:
|
|
@@ -623,15 +751,24 @@ if (changed.has("value") && this.refs.input.value !== v.value) this.refs.input.v
|
|
|
623
751
|
|
|
624
752
|
## 效能
|
|
625
753
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
|
629
|
-
|
|
630
|
-
| 1,000 |
|
|
631
|
-
| 5,000 |
|
|
632
|
-
| 10,000 |
|
|
633
|
-
|
|
634
|
-
|
|
754
|
+
`npm run bench` 會在真實 Chrome 跑大型清單的基準測試並印出這張表。每個項目顯示一段文字;時間是 5 次的中位數,只計腳本執行時間(不含版面與繪製)。這是在一台機器(WSL 上的 Chrome)量測的結果,請看比例,不要當成絕對數值。「直接操作 DOM」是用原生 DOM 呼叫手寫的 keyed 清單,也就是任何函式庫成本的下限。
|
|
755
|
+
|
|
756
|
+
| 做法 | 項目數 | 建立 | 更新 10% | 反轉順序 | 刪除一半 | 追加 1,000 項 |
|
|
757
|
+
|---|---|---|---|---|---|---|
|
|
758
|
+
| 元件(`items =`) | 1,000 | 15.4 ms | 0.5 ms | 1.8 ms | 1.1 ms | 14.6 ms |
|
|
759
|
+
| 元件(`items =`) | 5,000 | 80.5 ms | 1.6 ms | 10 ms | 5.4 ms | 17.6 ms |
|
|
760
|
+
| 元件(`items =`) | 10,000 | 160.6 ms | 3.2 ms | 21 ms | 11.3 ms | 16 ms |
|
|
761
|
+
| 純元素(`replace()`) | 1,000 | 2.8 ms | 0.1 ms | 0.7 ms | 0.2 ms | 2.9 ms |
|
|
762
|
+
| 純元素(`replace()`) | 5,000 | 15.8 ms | 0.7 ms | 4.3 ms | 1.7 ms | 3.7 ms |
|
|
763
|
+
| 純元素(`replace()`) | 10,000 | 42 ms | 1.4 ms | 8.6 ms | 3.3 ms | 4.4 ms |
|
|
764
|
+
| 直接操作 DOM(基準線) | 1,000 | 0.8 ms | 0.2 ms | 1 ms | 0.3 ms | 0.8 ms |
|
|
765
|
+
| 直接操作 DOM(基準線) | 5,000 | 4 ms | 0.8 ms | 4.3 ms | 1.6 ms | 1.1 ms |
|
|
766
|
+
| 直接操作 DOM(基準線) | 10,000 | 8.7 ms | 1.7 ms | 10.9 ms | 3.9 ms | 1.6 ms |
|
|
767
|
+
|
|
768
|
+
- 元件項目的建立成本約每項 15 µs,因為每個項目都是有自己範本、refs 與生命週期的自訂元素;純元素約 3–4 µs,直接操作 DOM 約 1 µs。
|
|
769
|
+
- 項目建立之後,更新、重排與刪除都在直接操作 DOM 的兩倍左右以內:只會處理換成新物件的項目,保留的元素是搬動而不是重建。
|
|
770
|
+
- 一個畫面約 16 ms。幾千個元件項目的清單沒有問題;上萬項時請改用 `replace()` 的純元素、一次只顯示一頁或一段範圍,或只讓需要狀態與事件的列使用元件項目。
|
|
771
|
+
- 事件委派不會隨項目增加而多掛監聽器。
|
|
635
772
|
|
|
636
773
|
## 範例
|
|
637
774
|
|
|
@@ -672,6 +809,33 @@ function menuItem(label: string): HTMLLIElement {
|
|
|
672
809
|
|
|
673
810
|
## 清單
|
|
674
811
|
|
|
812
|
+
填清單有兩種方式,用一條規則決定:**項目有自己的東西(狀態、事件、內層清單)就是元件,用 `items =` 填入;項目只是幾個文字與 `data-if` 就是純元素,用 `replace()` 填入。** 範例 4 示範前者,範例 2 示範後者。
|
|
813
|
+
|
|
814
|
+
### 項目是元件:`items = ...`
|
|
815
|
+
|
|
816
|
+
在 `<template data-each>` 上寫出識別項目的欄位名稱與接收項目的屬性名稱,項目是一個自訂元素:
|
|
817
|
+
|
|
818
|
+
```html
|
|
819
|
+
<ul>
|
|
820
|
+
<template data-each="todos" data-key="id" data-prop="todo">
|
|
821
|
+
<todo-row role="listitem" data-on-toggled="toggle"></todo-row>
|
|
822
|
+
</template>
|
|
823
|
+
</ul>
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
```ts
|
|
827
|
+
render(view, changed) {
|
|
828
|
+
if (changed.has("todos")) this.lists.todos.items = view.todos;
|
|
829
|
+
}
|
|
830
|
+
toggle(_e: Event, hit: EventHit) { /* hit.key 就是該項目的 id */ }
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
指派 `items` 時:key 還在的項目保留原本的元素(瀏覽器支援時用 `moveBefore` 搬動,所以焦點與狀態不會丟)、新 key 建立元素、消失的 key 移除,並對「與上次不是同一個物件」的項目設定元素的 `todo` 屬性。請用不可變更新:就地修改已指派過的物件欄位再指派同一個物件,項目元件不會更新,因為 props 以參考比較;請改指派新物件,例如 `{ ...todo, done: true }`。項目元件在自己的 `props` 宣告 `todo`,愛怎麼顯示都可以,用 `emit` 通知父元件,父元件在處理方法裡讀 `hit.key`。讀取 `items` 會回傳最近一次指派的內容。`tsc` 會檢查這個指派:每個項目要符合 `<todo-row>` 宣告的 `todo` 屬性型別並帶有 `data-key` 欄位(`id`),項目元件必須宣告 `data-prop` 指定的屬性;沒有 `data-key` 與 `data-prop` 的清單不接受指派。這要靠元件註冊在 `ts-dom-tags.ts`(掃描目錄內有匯出的 `define("todo-row", TodoRow)`)。元件定義在掃描目錄內、但類別沒有匯出,或 `ts-dom-tags.ts` 還沒列出它時,指派會是型別錯誤並說明原因(`itemComponentClassNotExported`、`itemComponentMissingFromTagMap`);只有掃描目錄以外的元件,才接受任何帶有 `id` 欄位的項目。執行期則在指派的不是物件陣列、項目沒有字串或數字的 `id`、兩個項目 key 重複、或範本沒有 `data-key` 與 `data-prop` 時丟錯。
|
|
834
|
+
|
|
835
|
+
在 `<ul>` 或 `<ol>` 裡,項目元件要加 `role="listitem"`,因為自訂元素不是 `<li>`;無障礙檢查(`list-structure`)會要求這點。`data-each` 刻意不能巢狀:一個項目就是一個元件,項目裡的清單就是該元件自己的 `data-each`。這樣每套範本都只有一層,有自己的 `refs`、`text` 與事件處理。
|
|
836
|
+
|
|
837
|
+
### 項目是純元素:`replace()`
|
|
838
|
+
|
|
675
839
|
```html
|
|
676
840
|
<ul>
|
|
677
841
|
<template data-each="tags">
|
|
@@ -708,9 +872,16 @@ this.lists.tags.replace(
|
|
|
708
872
|
- `data-each` 不能巢狀,要分成兩套模板:把內層清單放進自己的 `.html`(有自己 `data-each` 的子元件),在外層項目裡使用該元件的標籤。
|
|
709
873
|
- `<slot>` 不能放在項目內。
|
|
710
874
|
|
|
875
|
+
### 項目的搬動、連接與訂閱
|
|
876
|
+
|
|
877
|
+
- 瀏覽器支援 `moveBefore` 時,被搬動的項目保持連接:`disconnected()` 與 `connected()` 都不會執行,焦點、捲動位置與訂閱都不受影響。
|
|
878
|
+
- 不支援時,被搬動的項目會先移出再放回:`disconnected()` 執行並釋放所有用 `bind`、`on`、`onCleanup` 登記的東西,接著 `connected()` 再執行一次。元素本身、props 與 state 都不變。
|
|
879
|
+
- 所以訂閱要寫在 `connected()`(`this.bind(store, ...)`、`this.on(...)`),不要寫在建構子或欄位初始值:這樣在任何瀏覽器搬動之後都會重新訂閱。寫在建構子的訂閱,第一次搬動就被釋放,之後不會再回來。
|
|
880
|
+
- 每個項目都需要的資料(主題、目前使用者、登記表)放進 `createStore`,由項目元件在 `connected()` 裡 bind,不要複製進每個項目物件。
|
|
881
|
+
|
|
711
882
|
### 清單更新的順序
|
|
712
883
|
|
|
713
|
-
`replace()` 與 `upsert()`
|
|
884
|
+
指派 `items` 時,新項目的 `data-prop` 屬性會在元素放進文件之前就設定好,所以項目元件連接時已經有自己的項目,第一次渲染就會顯示出來。`replace()` 與 `upsert()` 則是先把新項目的元素放進文件,再呼叫 `update`,所以放在這種項目內的子元件會在父元件設定屬性之前就已經連接;這時每列自己的狀態請等子元件第一次拿到資料時再建立(例如在屬性的 setter 裡),`connected()` 裡的渲染要先確認需要的資料已經存在。
|
|
714
885
|
|
|
715
886
|
## Trusted Types、翻譯與圖示
|
|
716
887
|
|
|
@@ -726,9 +897,24 @@ this.lists.tags.replace(
|
|
|
726
897
|
|
|
727
898
|
## 檢查
|
|
728
899
|
|
|
900
|
+
每個工具只檢查自己看得到的部分,彼此不重複:
|
|
901
|
+
|
|
902
|
+
| 工具 | 檢查什麼 |
|
|
903
|
+
|---|---|
|
|
904
|
+
| oxfmt 這類格式化工具(`oxfmt --check` 或 `vp fmt --check`) | 範本標記是否結構正確(例如 `<p><div></p></div>` 這種錯置的結束標籤,在那裡是語法錯誤;而本函式庫使用的 HTML 解析器會悄悄把它修正成另一棵樹),以及排版是否一致。它不認識 `{{ }}` 或 `data-*` 的名稱。使用時請以 `collapse` 空白模式產生,否則重新排版可能改變畫面上的空白。 |
|
|
905
|
+
| `ts-dom gen` | 範本規則:洞、`data-each`、`data-if`、`data-on-*`、禁止的標記。 |
|
|
906
|
+
| `tsc` | HTML 裡的每個名稱都存在於元件中且型別正確,清單、props 與事件都對得上。 |
|
|
907
|
+
| `ts-dom lint` / oxlint 外掛 | HTML 宣告了但類別從未使用的東西、元件之間的資料流、無障礙。 |
|
|
908
|
+
| `ts-dom doctor` | 其他工具所依賴的專案設定。 |
|
|
909
|
+
| `ts-dom check` | 一次跑完 `gen`、`tsc` 與 `lint`,並把範本契約的錯誤指向範本。 |
|
|
910
|
+
|
|
729
911
|
四道檢查,互補。**每一則訊息都有穩定的規則代號,並說明哪裡錯、為什麼、怎麼修**;`npx ts-dom rules` 列出全部規則。
|
|
730
912
|
|
|
731
|
-
|
|
913
|
+
**懷疑是設定本身有問題時:`npx ts-dom doctor [dirs...]`。** 它檢查專案在任何檢查能運作之前需要的東西:tsconfig 要開 `strict`、含 DOM lib、模組解析能把 `./x.html.js` 對到產生檔、`include` 涵蓋範本資料夾;runtime 套件已安裝;範本以 `./x.html.js` 匯入(不是 `./x.html`);產生檔是否過期;Vite 外掛與 oxlint 外掛是否設定;以及設定了 oxfmt 或 Prettier 這類格式化工具時,範本是否以 `collapse` 空白模式產生。每則都有穩定的規則代號與 `Fix:` 提示,`--json` 以資料形式輸出。
|
|
914
|
+
|
|
915
|
+
**一個指令跑完全部:`npx ts-dom check [dirs...]`。** 它會回報範本診斷、重新產生產生檔(加 `--no-write` 則改為回報過期的產生檔)、執行專案自己的 `tsc`(TypeScript 5、6、7 皆可;`--project=` 指定 tsconfig),並跑 lint 規則(`--a11y` 加上無障礙檢查)。來自範本契約的 TypeScript 錯誤會指向範本,而不是 `Component(...)` 那一行:`{{ title }}` 找不到任何名為 `title` 的東西時,會以 `missing-binding` 回報在 `.html` 的行列;缺少的處理方法則是 `missing-handler`;項目元件的問題各有自己的規則代號;只是因為類別已經壞掉而連帶產生的錯誤會被略過。`--json` 以資料形式輸出,有錯誤時結束碼為 1(加 `--strict` 時警告也算)。
|
|
916
|
+
|
|
917
|
+
1. **codegen 診斷**(`ts-dom gen` 與 Vite 外掛,帶行列號,有錯誤時中斷、不寫檔):重複的 `data-ref`、`data-each` 沒有單一根元素、`data-each` 不在 `<template>` 上、`data-each` 巢狀、`data-key` 與 `data-prop` 不完整或不合法、有 `data-key` 的清單其項目不是單一自訂元素、`data-if` 在項目根元素、`<slot>` 在項目內、`<style>` 不在第一層、不合法的洞、屬性內的洞、不合法的 `data-on-*`(含與 DOM 或 ts-dom 成員同名),範本內的 `<html>`、`<head>`、`<body>`,以及會執行程式碼或被 Trusted Types 擋下的標記:`<script>`、`on*` 屬性、`srcdoc`、`javascript:` 網址。
|
|
732
918
|
2. **tsc**:TypeScript 使用了 HTML 沒有宣告的名稱就報錯(`this.refs.nope`、`this.show("nope", true)`);`state`、`props`、`derive`、`emits` 與 HTML 之間的所有約束(見「資料流」);子元件 prop 的名稱與型別;`define()` 檢查 `data-on-*` 的方法存在。
|
|
733
919
|
3. **執行期**:`Component()` 與元件在定義、連接與使用時,對同樣的問題丟出帶 `[規則代號]` 與修正方式的錯誤。
|
|
734
920
|
4. **lint**(`ts-dom/unused-markup`、`ts-dom/data-flow`、`ts-dom/a11y`):HTML 宣告了卻沒被使用的標記(錯誤,附 HTML 位置)、資料流建議與子元件契約(警告)、ARIA(警告)。
|