@fui-org/fui-cli 1.3.1 → 2.0.0

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.
Files changed (111) hide show
  1. package/README.md +19 -3
  2. package/dist/fui-bmg5pnmq.js +379 -0
  3. package/dist/fui.js +1 -1
  4. package/package.json +7 -6
  5. package/skills/fui/SKILL.md +9 -41
  6. package/skills/fui-skill/SKILL.md +95 -225
  7. package/skills/fui-skill/assets/projectdefaultstyle-3.0.css +555 -0
  8. package/skills/fui-skill/assets/projectdefaultstyle.css +207 -235
  9. package/skills/fui-skill/references/INDEX.md +105 -137
  10. package/skills/fui-skill/references/advanced-techniques.md +76 -68
  11. package/skills/fui-skill/references/coding-standards.md +56 -56
  12. package/skills/fui-skill/references/component-design.md +166 -173
  13. package/skills/fui-skill/references/component-quickref.md +61 -60
  14. package/skills/fui-skill/references/component-table.md +128 -117
  15. package/skills/fui-skill/references/components-dialog.md +55 -56
  16. package/skills/fui-skill/references/components-display.md +24 -30
  17. package/skills/fui-skill/references/components-echart.md +186 -261
  18. package/skills/fui-skill/references/components-input.md +72 -96
  19. package/skills/fui-skill/references/controls-patterns.md +196 -342
  20. package/skills/fui-skill/references/controls-styling-vocabulary.md +130 -97
  21. package/skills/fui-skill/references/db-table-design.md +24 -28
  22. package/skills/fui-skill/references/db-workflow.md +191 -390
  23. package/skills/fui-skill/references/default-function.md +169 -128
  24. package/skills/fui-skill/references/design-modes.md +35 -63
  25. package/skills/fui-skill/references/echart-templates.md +204 -196
  26. package/skills/fui-skill/references/fastproject.md +62 -60
  27. package/skills/fui-skill/references/fsheet.md +109 -124
  28. package/skills/fui-skill/references/fullstack-workflow.md +90 -128
  29. package/skills/fui-skill/references/module-data-patterns.md +31 -40
  30. package/skills/fui-skill/references/module-json-anatomy.md +47 -52
  31. package/skills/fui-skill/references/module-structure.md +80 -196
  32. package/skills/fui-skill/references/new-session.md +49 -51
  33. package/skills/fui-skill/references/pdfmake.md +17 -17
  34. package/skills/fui-skill/references/permission-system.md +89 -108
  35. package/skills/fui-skill/references/platform-architecture.md +128 -153
  36. package/skills/fui-skill/references/project-config.md +102 -134
  37. package/skills/fui-skill/references/project-provisioning.md +139 -244
  38. package/skills/fui-skill/references/script-map.md +208 -242
  39. package/skills/fui-skill/references/sql-clr-functions.md +98 -97
  40. package/skills/fui-skill/references/system-design.md +63 -88
  41. package/skills/fui-skill/references/tapi-file-api.md +46 -52
  42. package/skills/fui-skill/references/tapi-permission-patterns.md +51 -53
  43. package/skills/fui-skill/references/tapi-reference.md +132 -207
  44. package/skills/fui-skill/references/tools-registry.md +84 -460
  45. package/skills/fui-skill/references/ui-crosswindow-patterns.md +79 -75
  46. package/skills/fui-skill/references/ui-dialog-patterns.md +98 -72
  47. package/skills/fui-skill/references/ui-layout-patterns.md +26 -26
  48. package/skills/fui-skill/references/ui-patterns.md +71 -83
  49. package/skills/fui-skill/references/ui-screenshot-review.md +63 -62
  50. package/skills/fui-skill/references/ui-table-cell-patterns.md +61 -59
  51. package/skills/fui-skill/references/ui-templates.md +16 -23
  52. package/skills/fui-skill/references/verification.md +236 -246
  53. package/skills/fui-skill/references/watcher-patterns.md +30 -63
  54. package/skills/fui-skill/references/websocket-realtime.md +83 -66
  55. package/skills/fui-skill/scripts/component-3.0.js +298 -131
  56. package/skills/fui-skill/scripts/component.js +277 -271
  57. package/skills/fui-skill/scripts/componentTable-3.0.js +182 -53
  58. package/skills/fui-skill/scripts/componentTable.js +171 -49
  59. package/skills/fui-skill/scripts/defaultfunction-3.0.js +88 -3
  60. package/skills/fui-skill/scripts/defaultfunction.js +88 -3
  61. package/skills/fui-skill/scripts/fsheet.js +38 -0
  62. package/dist/fui-x7kph78r.js +0 -420
  63. package/skills/fui-skill/README.md +0 -112
  64. package/skills/fui-skill/metadata.json +0 -75
  65. package/skills/fui-skill-cli/SKILL.md +0 -139
  66. package/skills/fui-skill-cli/references/INDEX.md +0 -110
  67. package/skills/fui-skill-cli/references/advanced-techniques.md +0 -168
  68. package/skills/fui-skill-cli/references/coding-standards.md +0 -112
  69. package/skills/fui-skill-cli/references/component-design.md +0 -448
  70. package/skills/fui-skill-cli/references/component-quickref.md +0 -78
  71. package/skills/fui-skill-cli/references/component-table.md +0 -248
  72. package/skills/fui-skill-cli/references/components-dialog.md +0 -191
  73. package/skills/fui-skill-cli/references/components-display.md +0 -141
  74. package/skills/fui-skill-cli/references/components-echart.md +0 -316
  75. package/skills/fui-skill-cli/references/components-input.md +0 -335
  76. package/skills/fui-skill-cli/references/controls-patterns.md +0 -701
  77. package/skills/fui-skill-cli/references/controls-styling-vocabulary.md +0 -137
  78. package/skills/fui-skill-cli/references/db-table-design.md +0 -73
  79. package/skills/fui-skill-cli/references/db-workflow.md +0 -288
  80. package/skills/fui-skill-cli/references/default-function.md +0 -425
  81. package/skills/fui-skill-cli/references/design-modes.md +0 -57
  82. package/skills/fui-skill-cli/references/echart-templates.md +0 -489
  83. package/skills/fui-skill-cli/references/fastproject.md +0 -99
  84. package/skills/fui-skill-cli/references/fsheet.md +0 -203
  85. package/skills/fui-skill-cli/references/fullstack-workflow.md +0 -313
  86. package/skills/fui-skill-cli/references/module-data-patterns.md +0 -117
  87. package/skills/fui-skill-cli/references/module-json-anatomy.md +0 -132
  88. package/skills/fui-skill-cli/references/module-structure.md +0 -141
  89. package/skills/fui-skill-cli/references/new-session.md +0 -85
  90. package/skills/fui-skill-cli/references/pdfmake.md +0 -60
  91. package/skills/fui-skill-cli/references/permission-system.md +0 -150
  92. package/skills/fui-skill-cli/references/platform-architecture.md +0 -269
  93. package/skills/fui-skill-cli/references/project-config.md +0 -303
  94. package/skills/fui-skill-cli/references/project-provisioning.md +0 -278
  95. package/skills/fui-skill-cli/references/script-map.md +0 -262
  96. package/skills/fui-skill-cli/references/sql-clr-functions.md +0 -225
  97. package/skills/fui-skill-cli/references/system-design.md +0 -89
  98. package/skills/fui-skill-cli/references/tapi-file-api.md +0 -185
  99. package/skills/fui-skill-cli/references/tapi-permission-patterns.md +0 -156
  100. package/skills/fui-skill-cli/references/tapi-reference.md +0 -474
  101. package/skills/fui-skill-cli/references/tools-registry.md +0 -84
  102. package/skills/fui-skill-cli/references/ui-crosswindow-patterns.md +0 -321
  103. package/skills/fui-skill-cli/references/ui-dialog-patterns.md +0 -255
  104. package/skills/fui-skill-cli/references/ui-layout-patterns.md +0 -176
  105. package/skills/fui-skill-cli/references/ui-patterns.md +0 -303
  106. package/skills/fui-skill-cli/references/ui-screenshot-review.md +0 -95
  107. package/skills/fui-skill-cli/references/ui-table-cell-patterns.md +0 -318
  108. package/skills/fui-skill-cli/references/ui-templates.md +0 -22
  109. package/skills/fui-skill-cli/references/verification.md +0 -236
  110. package/skills/fui-skill-cli/references/watcher-patterns.md +0 -163
  111. package/skills/fui-skill-cli/references/websocket-realtime.md +0 -271
@@ -1,56 +1,48 @@
1
- # Controls Patterns & Logic
1
+ # Controls Patterns & Logic
2
2
 
3
- > File này sở hữu: **value resolution (backtick / `{{ }}` / JS eval), action engine (API/IN/OUT/CALLBACK/CONFIRM/MESS/CALL/IF/EXE), key notation, grid wrapper `container > rows > cols`, control object, layout rules**. Bộ class/style được duyệt xem [controls-styling-vocabulary.md](controls-styling-vocabulary.md); tổng quan 4 thành phần module.json xem [module-json-anatomy.md](module-json-anatomy.md). *(Tier A — luôn nạp cùng `skill_get`.)*
4
-
5
- Guide to defining logic and layout in `module.json`.
3
+ > Owns: value resolution (backtick / `{{ }}` / JS eval), action engine (API/IN/OUT/CALLBACK/CONFIRM/MESS/CALL/IF/EXE), key notation, grid wrapper `container > rows > cols`, control object, layout rules. Classes/styles: [controls-styling-vocabulary.md](controls-styling-vocabulary.md); module.json overview: [module-json-anatomy.md](module-json-anatomy.md). _(Tier A — loaded with `fui skill get`.)_
6
4
 
7
5
  ---
8
6
 
9
- ## Ưu tiên FUI components — kiểm tra trước khi dùng `v-*`
10
-
11
- FUI đã xây dựng sẵn các `f-*` component với nhiều tính năng mặc định (sort, search, CRUD, inline action, form validation, ...) — dùng `f-*` sẽ ít code hơn và tận dụng được toàn bộ tích hợp runtime. Chỉ dùng `v-*` khi FUI chưa có equivalent phù hợp.
7
+ ## Prefer `f-*` over `v-*`
12
8
 
13
- | Ưu tiên dùng | Thay vì | Lý do ưu tiên |
14
- |---|---|---|
15
- | `f-table` | `v-data-table` (CRUD lẫn readonly) | sort/search/inline action/CRUD tích hợp sẵn; readonly = không khai `:update-api`; tự build headers khi bỏ `:headers`; hàng tổng qua `sum-format` |
16
- | `f-search` | `v-autocomplete` | API search, debounce, loading state có sẵn |
17
- | `f-button` | `v-btn` (khi cần action FUI) | hỗ trợ CALL, hotkey, form validation |
18
- | `f-date` / `f-time` | `v-text-field` + date parsing | picker + typed mask tích hợp sẵn |
19
- | `f-menu` | `v-menu + v-btn` dropdown | cấu hình đơn giản hơn, tích hợp icon/action/link |
20
- | `f-echart` | `<canvas>`, `v-chart`, lib khác | ECharts V5 — hỗ trợ mọi loại chart |
9
+ Use `v-*` only when no `f-*` equivalent exists (check [component-quickref.md](component-quickref.md)); `f-*` has built-in features and runtime integration.
21
10
 
22
- > Trước khi dùng `v-*`, kiểm tra [component-quickref.md](component-quickref.md) — nếu có `f-*` equivalent thì ưu tiên dùng `f-*`.
11
+ | Use | Instead of | Why |
12
+ | ------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `f-table` | `v-data-table` (CRUD and readonly) | built-in sort/search/inline action/CRUD; readonly = omit `:update-api`; auto headers when `:headers` omitted; totals via `sum-format` |
14
+ | `f-search` | `v-autocomplete` | API search, debounce, loading |
15
+ | `f-button` | `v-btn` (FUI action) | CALL, hotkey, form validation |
16
+ | `f-date` / `f-time` | `v-text-field` + parsing | picker + typed mask |
17
+ | `f-menu` | `v-menu + v-btn` | simpler, icon/action/link |
18
+ | `f-echart` | `<canvas>`, `v-chart`, other libs | ECharts V5, all chart types |
23
19
 
24
- **Default attrs tự động** — FUI inject sẵn default props cho mỗi `f-*` component (xem `defaultControlAttr` trong `scripts/component.js`). Chỉ khai báo prop khi muốn **override** giá trị mặc định, không cần lặp lại default:
20
+ Defaults auto-injected (`defaultControlAttr` in `scripts/component.js`) — declare a prop only to override:
25
21
 
26
- | Component | Default đã có sẵn (không cần khai báo lại) |
27
- |---|---|
28
- | `f-table` | `:dense true` · `:show-search true` · `:fixed-header true` · `:items-per-page 50` · `:hide-default-footer true` · `mobile-breakpoint 0` |
29
- | `f-button` | `color primary` · `elevation 0` |
30
- | `f-date` / `f-time` | `:dense true` · `:outlined true` · `:hide-details true` |
31
- | `f-search` | `:dense true` · `:outlined true` · `:hide-details true` · `:auto-select-first true` |
32
- | `f-menu` | `color primary` · `:depressed true` |
22
+ | Component | Defaults |
23
+ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `f-table` | `:dense true` · `:show-search true` · `:fixed-header true` · `:items-per-page 50` · `:hide-default-footer true` · `mobile-breakpoint 0` |
25
+ | `f-button` | `color primary` · `elevation 0` |
26
+ | `f-date` / `f-time` | `:dense true` · `:outlined true` · `:hide-details true` |
27
+ | `f-search` | `:dense true` · `:outlined true` · `:hide-details true` · `:auto-select-first true` |
28
+ | `f-menu` | `color primary` · `:depressed true` |
33
29
 
34
- > **Cảnh báo CRUD:** Không dùng `f-button` + `f-dialog` để mở form thêm mới — không hoạt động ổn định trong FUI V2. Dùng `ctrl-update` của `f-table` (`:update-form` + `:update-api`) cho mọi CRUD inline. Khi cần dialog riêng (form có logic đặc biệt, dynamic controls, multi-step...) → dựng `v-dialog` thủ công theo anatomy chuẩn phong cách f-dialog (`design_read("fui")` §4, `ui-dialog-patterns.md`).
30
+ > **CRUD:** never `f-button` + `f-dialog` for an add form (unstable in V2). Use `f-table` `ctrl-update` (`:update-form` + `:update-api`). Special dialogs (custom logic, dynamic controls, multi-step) → hand-built `v-dialog` in f-dialog anatomy (`fui design read fui` §4, `ui-dialog-patterns.md`).
35
31
 
36
32
  ---
37
33
 
38
34
  ## 0. Value Resolution Rules (Fundamental)
39
35
 
40
- FUI dùng hàm `getVueData(value, src)` để giải thích mọi string value trong `data[]` và `IN` mapping. Hiểu đúng quy tắc này tránh lỗi set data sai.
41
-
42
- ### Quy tắc xác định kiểu value
43
-
44
- | Dạng value | Cách xử lý | Kết quả |
45
- |---|---|---|
46
- | Primitive (number, boolean, object, array) | Lưu nguyên | Giá trị gốc |
47
- | String bắt đầu bằng `` ` `` | Lodash template (literal + `{{ }}` interpolation) | String sau khi render |
48
- | String có `{{ }}` | Lodash template với interpolation | String sau khi render |
49
- | String có spaces (không có backtick, không có `{{ }}`) | Lodash template → trả về literal | String đó |
50
- | String không có spaces, không backtick, không `{{ }}` | **Evaluated as JS expression** | Giá trị của expression |
51
- | `""` (empty string) | Empty string | `""` |
36
+ `getVueData(value, src)` resolves every string in `data[]` and `IN`:
52
37
 
53
- ### Ví dụ thực tế
38
+ | Value | Result |
39
+ | ------------------------------------------ | ----------------------------------- |
40
+ | Primitive (number, boolean, object, array) | as-is |
41
+ | String starting with `` ` `` | lodash template (literal + `{{ }}`) |
42
+ | String containing `{{ }}` | lodash template |
43
+ | String with spaces, no backtick/`{{ }}` | literal |
44
+ | String without spaces, backtick, `{{ }}` | **evaluated as JS** |
45
+ | `""` | `""` |
54
46
 
55
47
  ```json
56
48
  "data": [
@@ -59,10 +51,10 @@ FUI dùng hàm `getVueData(value, src)` để giải thích mọi string value t
59
51
  "b": 3, // number → 3
60
52
  "c": true, // boolean → true
61
53
  "d": null, // null → null
62
- "e": "val1", // no space → JS eval: giá trị của biến val1
54
+ "e": "val1", // no space → JS eval: value of variable val1
63
55
  "f": "val4.prop2", // JS eval: vueData.val4.prop2
64
- "g": "val5[1]", // JS eval: phần tử thứ 2 của array val5
65
- "h": "[val1]", // JS eval: tạo array [giá_trị_val1]
56
+ "g": "val5[1]", // JS eval: 2nd element of array val5
57
+ "h": "[val1]", // JS eval: array [value_of_val1]
66
58
  "i": "[1,2,3].map(x=>x*2)", // no space → JS eval → [2,4,6]
67
59
  "j": "`Xin chào {{user.UserName}}", // backtick → lodash template → "Xin chào Tuấn"
68
60
  "k": "tổng là {{a+b}} item", // {{ }} → lodash template → "tổng là 3 item"
@@ -71,13 +63,11 @@ FUI dùng hàm `getVueData(value, src)` để giải thích mọi string value t
71
63
  ]
72
64
  ```
73
65
 
74
- > **Quan trọng**: `"val1"` (no spaces) → FUI lấy GIÁ TRỊ của biến `val1`, KHÔNG phải string "val1". Muốn literal string "val1", phải dùng `` "`val1" ``.
66
+ `"val1"` = value of `val1`, not the string; literal needs ``"`val1"``.
75
67
 
76
- > ⚠️ **Bảng trên KHÔNG áp cho `:attr`.** Chỉ `data[]` và `IN` mapping mới đi qua `getVueData()`. Giá trị của một binding (`:option`, `:config`, mọi `v-bind:`) đi **thẳng vào template compiler của Vue**, nên luật khác hẳn: chỉ nhận **một biểu thức thuần**, cấm `function(){}`/`var`/`return`/`;`. Vi phạm ⇒ trang trắng với 0 JS error, 0 `[Vue warn]`. Luật đầy đủ + cách chuyển logic sang `script.js` + `EXE`: [advanced-techniques.md](advanced-techniques.md) §1.
68
+ > ⚠️ **Not for `:attr`.** Bindings (`:option`, `:config`, any `v-bind:`) go straight to Vue's compiler: one pure expression; no `function(){}`/`var`/`return`/`;`. Violation ⇒ blank page, 0 JS error, 0 `[Vue warn]`. Rules + moving logic to `script.js` + `EXE`: [advanced-techniques.md](advanced-techniques.md) §1.
77
69
 
78
- ### String interpolation với `{{ }}`
79
-
80
- `{{ }}` dùng lodash template — có thể chứa bất kỳ JS expression nào:
70
+ `{{ }}` takes any JS expression:
81
71
 
82
72
  ```json
83
73
  "label": "Còn {{items.length}} bản ghi",
@@ -87,61 +77,53 @@ FUI dùng hàm `getVueData(value, src)` để giải thích mọi string value t
87
77
 
88
78
  ---
89
79
 
90
- ## 1. Khai báo và gán dữ liệu trong `data[]`
91
-
92
- ### Key notation — Dot, Bracket, và Cross-window
80
+ ## 1. `data[]` keys and assignment
93
81
 
94
- Key trong `data[]` mapping không chỉ là tên biến đơn giản — FUI's `setValue` hỗ trợ đầy đủ path notation:
82
+ ### Key notation
95
83
 
96
84
  ```json
97
85
  "data": [
98
86
  {
99
- "val": "Hang So", // biến đơn
87
+ "val": "Hang So", // simple variable
100
88
  "obj.prop": "val1", // dot notation → vueData.obj.prop
101
89
  "obj.nested.deep": true, // multi-level dot
102
- "arr[0].name": "Tuan", // bracket với literal index
103
- "arr[val2].name": "abc", // bracket với biến index (val2 = 3 → arr[3])
104
- "arr[0][val1].x": "y", // bracket với variable key (val1 = "Hang So")
105
- "#PARENT.field": "val1", // ghi vào parent window
106
- "#Win1.field": "val1" // ghi vào iframe Win1
90
+ "arr[0].name": "Tuan", // bracket, literal index
91
+ "arr[val2].name": "abc", // bracket, variable index (val2 = 3 → arr[3])
92
+ "arr[0][val1].x": "y", // bracket, variable key (val1 = "Hang So")
93
+ "#PARENT.field": "val1", // write to parent window
94
+ "#Win1.field": "val1" // write to iframe Win1
107
95
  }
108
96
  ]
109
97
  ```
110
98
 
111
- FUI **tự tạo object/array trung gian** nếu chưa tồn tại — dùng khi cập nhật một field sâu mà không phải gán lại cả object. Cùng luật này áp cho đích của `OUT` và mọi fragment gán dữ liệu.
99
+ Missing intermediate objects/arrays are auto-created (also for `OUT` targets and assignment fragments) — no need to reassign the whole object. `#...` keys use `setWindowData` ([ui-patterns.md](ui-patterns.md) §Cross-window).
112
100
 
113
- > Khi key là `#...`, FUI gọi `setWindowData` thay vì `setValue` — xem [ui-patterns.md](ui-patterns.md) §Cross-window.
101
+ ### Events/slots in `attr` — full form only
114
102
 
115
- ### Event và Slot trong `attr` — luôn viết đầy đủ, không viết tắt
103
+ Every event/slot of every component, incl. custom `f-*` and `$emit()` events:
116
104
 
117
- Trong `module.json`, khi khai báo **event** hoặc **slot** cho bất kỳ component nào (kể cả `f-*` tùy biến như `f-contest-list`, không riêng `v-*`), luôn dùng dạng đầy đủ — không dùng ký hiệu viết tắt của Vue:
118
-
119
- | Loại | Viết tắt (Vue hỗ trợ trong `.vue`) | Dùng trong `module.json` |
120
- |---|---|---|
121
- | Event | `@click`, `@saved`, `@bất-kỳ-event-nào` | `v-on:click`, `v-on:saved`, `v-on:bất-kỳ-event-nào` |
122
- | Slot | `#default`, `#item.name` | `v-slot:default`, `v-slot:item.name` |
105
+ | Kind | Shorthand (`.vue` only) | `module.json` |
106
+ | ----- | -------------------------------- | -------------------------------------------- |
107
+ | Event | `@click`, `@saved`, `@any-event` | `v-on:click`, `v-on:saved`, `v-on:any-event` |
108
+ | Slot | `#default`, `#item.name` | `v-slot:default`, `v-slot:item.name` |
123
109
 
124
110
  ```json
125
- // ❌ SAI — viết tắt, sẽ lỗi không load được
111
+ // ❌ WRONG — shorthand, fails to load
126
112
  { "el": "f-contest-list", "attr": { "@saved": "CALL(apiLoadContests)" } }
127
113
 
128
- // ✅ ĐÚNG
114
+ // ✅ RIGHT
129
115
  { "el": "f-contest-list", "attr": { "v-on:saved": "CALL(apiLoadContests)" } }
130
116
  ```
131
117
 
132
- Quy tắc này áp dụng cho **mọi event/slot của mọi component**, kể cả custom event do component tự `$emit()` (như `saved` ở trên) — không chỉ riêng DOM event chuẩn như `click`.
133
-
134
- > Không nhầm với `#PARENT.field` / `#Win1.field` — đó là cú pháp `#` riêng của FUI cho cross-window trong **`data[]`** (xem trên), khác hoàn toàn với slot shorthand `#` của Vue trong `attr`.
118
+ `#PARENT.field` / `#Win1.field` in `data[]` is FUI cross-window syntax, unrelated to Vue slot `#`.
135
119
 
136
- ### Gán giá trị liên kết (alias)
120
+ ### Alias and JS init
137
121
 
138
122
  ```json
139
- "GanLienGiaTri": "val1" // GanLienGiaTri = giá trị hiện tại của val1
123
+ "GanLienGiaTri": "val1" // GanLienGiaTri = current value of val1
140
124
  ```
141
125
 
142
- Không reactive — chỉ snapshot tại thời điểm `data[]` chạy. Muốn reactive → dùng `watch`.
143
-
144
- ### Data initialization với JS expressions
126
+ Snapshot at `data[]` run, not reactive — use `watch` for reactivity.
145
127
 
146
128
  ```json
147
129
  "data": [
@@ -156,22 +138,16 @@ Không reactive — chỉ snapshot tại thời điểm `data[]` chạy. Muốn
156
138
 
157
139
  ---
158
140
 
159
- ## 2. Component Actions (`data`)
160
-
161
- Actions are the "methods" of your module. They handle APIs, dialogs, and logic flow.
162
- Khai báo action thành object có tên trong `data`, gọi lại bằng `CALL`.
141
+ ## 2. Actions (`data`)
163
142
 
164
- ### QUY TẮC NỀN — một object `{}` là MỘT lệnh; nhiều lệnh có thứ tự là MỘT MẢNG `[]`
143
+ Declare named action objects in `data`; invoke with `CALL`.
165
144
 
166
- Đây là luật gốc của FUI, đọc trước mọi thứ khác trong mục này:
145
+ ### CORE RULE — one `{}` = ONE command; sequence = ARRAY `[]`
167
146
 
168
- > **Một lệnh / một tập lệnh = một object JSON `{}`** — vì bên trong một tập lệnh còn có thể có nhiều **tham số** và **tập lệnh con** lồng đệ quy (`IN`, `OUT`, `CALLBACK`, `THEN`/`ELSE`…).
169
- > **Nhiều lệnh chạy tuần tự = một MẢNG các object `[{…}, {…}, {…}]`** — mảng là cách DUY NHẤT diễn đạt "trước, rồi sau".
170
-
171
- Nhét nhiều lệnh vào chung một `{}` **không phải là "viết gọn"** — nó làm rơi lệnh:
147
+ An object may hold parameters and nested sub-actions (`IN`, `OUT`, `CALLBACK`, `THEN`/`ELSE`…) but runs one command. An array is the ONLY way to sequence.
172
148
 
173
149
  ```json
174
- // ❌ SAI — chỉ CALL chạy. EXE và MESS KHÔNG BAO GIỜ chạy.
150
+ // ❌ WRONG — only CALL runs. EXE and MESS NEVER run.
175
151
  "CALLBACK": {
176
152
  "EXE": "vueData.previewRows = []; vueData.excelRaw = [];",
177
153
  "CALL": "getList",
@@ -180,7 +156,7 @@ Nhét nhiều lệnh vào chung một `{}` **không phải là "viết gọn"**
180
156
  ```
181
157
 
182
158
  ```json
183
- // ✅ ĐÚNG — ba lệnh, ba object, chạy đúng thứ tự đã viết.
159
+ // ✅ RIGHT — three commands, three objects, run in written order.
184
160
  "CALLBACK": [
185
161
  { "EXE": "vueData.previewRows = []; vueData.excelRaw = [];" },
186
162
  { "CALL": "getList" },
@@ -188,63 +164,52 @@ Nhét nhiều lệnh vào chung một `{}` **không phải là "viết gọn"**
188
164
  ]
189
165
  ```
190
166
 
191
- **Vì sao rơi** — `vueAction()` (`fastproject.js:407`, `fastproject-3.0.js:448`, giống hệt nhau ở V2 và V3) dò các key lệnh theo **thứ tự cố định**, và **mỗi nhánh `return` ngay**:
167
+ `vueAction()` (`fastproject.js:407`, `fastproject-3.0.js:448`, same in V2/V3) checks keys in fixed order, each branch returns:
192
168
 
193
169
  ```
194
170
  IN (tham số, không return) → API ⏎ → CALL ⏎ → EXE ⏎ → FUN ⏎ → IF ⏎ → MESS ⏎ → MESSBOX ⏎ → (fallthrough) mapData
195
171
  ```
196
172
 
197
- Nên trong một object, **đúng một lệnh chạy** — lệnh có ưu tiên cao nhất, **không phải lệnh viết trước**. Ở ví dụ sai trên, `CALL` thắng `EXE` và `MESS` dù được viết ở giữa. Đảo thứ tự các dòng cũng không đổi được gì.
198
-
199
- Ngược lại, `runAction()` nhận mảng thì `_.forEach` từng phần tử qua `vueAction` ⇒ **mảng = tuần tự, giữ đúng thứ tự viết**.
200
-
201
- **Đây là lỗi hỏng-âm-thầm**: không JS error, không `[Vue warn]`, API vẫn chạy, chỉ là hai lệnh kia biến mất. Triệu chứng thường gặp: "sau khi lưu, danh sách có reload nhưng không thấy toast", hoặc "toast hiện mà form không reset".
202
-
203
- > Mọi chỗ nhận action đều theo luật này: `CALLBACK`, `ERRORCALLBACK`, `THEN`/`ELSE`, `CANCEL`, `:action`, `:update-api.new/edit/delete`, `watch`, `onclose`, và named action trong `data[]`. **Nghi ngờ thì bọc mảng** — mảng một phần tử `[{…}]` luôn hợp lệ.
204
-
205
- #### Key nào là LỆNH, key nào là THAM SỐ
206
-
207
- Chỉ nhóm "lệnh" mới bị luật một-object-một-lệnh. Tham số thì đi kèm lệnh trong **cùng** object là đúng.
173
+ Highest priority wins, not first written; reordering changes nothing. `runAction()` runs arrays in order (`_.forEach`). Silent failure (no error, no `[Vue warn]`); symptoms: "list reloads but no toast", "toast shows but form not reset".
208
174
 
209
- | Nhóm | Key | Ghi chú |
210
- |---|---|---|
211
- | **Lệnh** (mỗi object chỉ chạy 1) | `API` · `CALL` · `EXE` · `FUN` · `IF` · `MESS` · `MESSBOX` | theo đúng thứ tự ưu tiên ở trên |
212
- | **Tham số** (đi chung, không tính là lệnh) | `IN` · `OUT` · `HEADER` · `METHOD` · `CALLBACK` · `ERROROUT` · `ERRORCALLBACK` · `THEN` · `ELSE` · `CONFIRM` · `CANCEL` | `CONFIRM`/`CANCEL` được `runAction` xử lý một tầng trên rồi mới gọi `vueAction` |
175
+ Applies to `CALLBACK`, `ERRORCALLBACK`, `THEN`/`ELSE`, `CANCEL`, `:action`, `:update-api.new/edit/delete`, `watch`, `onclose`, named actions. When in doubt wrap: `[{…}]` is always valid.
213
176
 
214
- **Ngoại lệ duy nhất — `MESS` đi kèm `API`:** `callAPI` tự bắn toast khi thành công (`fastproject-3.0.js:755`, sau `OUT`, trước `CALLBACK`), nên ở đây `MESS` là **tham số của `API`**, không phải lệnh thứ hai. `{ "API": …, "IN": …, "MESS": …, "CALLBACK": … }` là đúng và rất thông dụng.
215
- Lưu ý: chỉ `MESS`, **không** áp cho `MESSBOX`; và chỉ với `API`, **không** với `FUN` (`runFunction` không đọc `MESS`).
177
+ | Group | Keys |
178
+ | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
179
+ | **Commands** (one per object, priority above) | `API` · `CALL` · `EXE` · `FUN` · `IF` · `MESS` · `MESSBOX` |
180
+ | **Parameters** (same object as command) | `IN` · `OUT` · `HEADER` · `METHOD` · `CALLBACK` · `ERROROUT` · `ERRORCALLBACK` · `THEN` · `ELSE` · `CONFIRM` · `CANCEL` (`CONFIRM`/`CANCEL` handled by `runAction` before `vueAction`) |
216
181
 
217
- #### Đừng trộn key gán dữ liệu vào cùng object với lệnh
182
+ **Sole exception:** `MESS` beside `API` is a parameter — `callAPI` toasts on success (`fastproject-3.0.js:755`, after `OUT`, before `CALLBACK`). `{ "API": …, "IN": …, "MESS": …, "CALLBACK": … }` is valid. Not `MESSBOX`; not with `FUN` (`runFunction` ignores `MESS`).
218
183
 
219
- Fragment gán dữ liệu (`{ "dlgOpen": false, "form.Name": "" }`) chạy ở **nhánh cuối cùng** của `vueAction` — nhánh này chỉ tới lượt khi **không** key lệnh nào khớp. Có lệnh trong cùng object thì mọi key gán bị bỏ im lặng:
184
+ Assignment fragments (`{ "dlgOpen": false, "form.Name": "" }`) run only in the fallthrough branch; any command in the same object silently drops them:
220
185
 
221
186
  ```json
222
- // ❌ SAI — CALL chạy, ba biến kia KHÔNG được gán
187
+ // ❌ WRONG — CALL runs, the three variables are NOT assigned
223
188
  { "PageNum": 1, "Description": "", "IsPaper": false, "CALL": "reload" }
224
189
 
225
- // ✅ ĐÚNG
190
+ // ✅ RIGHT
226
191
  [
227
192
  { "PageNum": 1, "Description": "", "IsPaper": false },
228
193
  { "CALL": "reload" }
229
194
  ]
230
195
  ```
231
196
 
232
- ### Bảng key của Action Protocol
197
+ ### Keys
233
198
 
234
- | Key | Mô tả |
235
- |---|---|
236
- | `API` | Endpoint cần gọi |
237
- | `IN` | Tham số đầu vào (tham chiếu `vueData.` hoặc `item.`) |
238
- | `OUT` | Lưu response vào biến (vd `"myList"`) — dạng object để tách nhiều SELECT |
239
- | `CALLBACK` | Action chạy sau khi thành công |
240
- | `ERROROUT` / `ERRORCALLBACK` | Bắt lỗi thủ công thay cho dialog lỗi mặc định |
241
- | `CONFIRM` / `CANCEL` | Hỏi xác nhận trước khi chạy / action khi user bấm Hủy |
242
- | `MESS` / `MESSBOX` | Toast / modal thông báo |
243
- | `CALL` | Gọi một named action khác |
244
- | `IF` / `THEN` / `ELSE` | Rẽ nhánh điều kiện |
245
- | `FUN` | Gọi hàm FUI có sẵn (`openWindow`, `showMessage`, ...) |
246
- | `ARRAY` + `COL` | Trích một cột từ mảng object thành mảng phẳng |
247
- | `EXE` | JS thô — dùng hạn chế |
199
+ | Key | Meaning |
200
+ | ---------------------------- | ------------------------------------------------------------ |
201
+ | `API` | endpoint |
202
+ | `IN` | input params (`vueData.` or `item.`) |
203
+ | `OUT` | store response (e.g. `"myList"`); object form splits SELECTs |
204
+ | `CALLBACK` | on success |
205
+ | `ERROROUT` / `ERRORCALLBACK` | manual error handling |
206
+ | `CONFIRM` / `CANCEL` | confirm first / on Cancel |
207
+ | `MESS` / `MESSBOX` | toast / modal |
208
+ | `CALL` | run named action |
209
+ | `IF` / `THEN` / `ELSE` | branch |
210
+ | `FUN` | FUI function (`openWindow`, `showMessage`, ...) |
211
+ | `ARRAY` + `COL` | column of object array → flat array |
212
+ | `EXE` | raw JS, sparingly |
248
213
 
249
214
  ```json
250
215
  "fetchUsers": {
@@ -255,9 +220,6 @@ Fragment gán dữ liệu (`{ "dlgOpen": false, "form.Name": "" }`) chạy ở *
255
220
  }
256
221
  ```
257
222
 
258
- Chi tiết từng key ở các mục bên dưới.
259
-
260
- ### Standard API Call
261
223
  ```json
262
224
  "apiLoadData": {
263
225
  "API": "/api/controller/action",
@@ -274,13 +236,12 @@ Chi tiết từng key ở các mục bên dưới.
274
236
  }
275
237
  ```
276
238
 
277
- > Hai lệnh ⇒ **mảng hai object**. Gộp `MESS` và `CALL` vào một `{}` thì chỉ `CALL` chạy — xem §QUY TẮC NỀN ở trên.
278
-
279
- > **Không gán nested object trực tiếp trong action:** `{ "form": { "field": "item.field" } }` sẽ không hoạt động — dùng dot notation cho từng field: `{ "form.field": "item.field" }`. Nếu đang dùng `ctrl-update`, không cần quản lý form thủ công.
239
+ - Two commands ⇒ array; merged, only `CALL` runs.
240
+ - No nested object assignment: `{ "form": { "field": "item.field" } }` fails; use `{ "form.field": "item.field" }`. `ctrl-update` needs no manual form handling.
280
241
 
281
- ### API URL Interpolation
242
+ ### URL interpolation
282
243
 
283
- `{{ }}` trong URL API được interpolate từ vueData:
244
+ `{{ }}` in `API` reads vueData:
284
245
 
285
246
  ```json
286
247
  "getDetail": {
@@ -293,9 +254,9 @@ Chi tiết từng key ở các mục bên dưới.
293
254
  }
294
255
  ```
295
256
 
296
- ### API Error Handling — ERROROUT / ERRORCALLBACK
257
+ ### ERROROUT / ERRORCALLBACK
297
258
 
298
- Mặc định, API lỗi sẽ hiện dialog lỗi toàn màn hình. Dùng `ERROROUT` + `ERRORCALLBACK` để bắt lỗi và xử lý thủ công:
259
+ Default: full-screen error dialog. With `ERROROUT` nothing shows automatically — you handle it.
299
260
 
300
261
  ```json
301
262
  "saveData": {
@@ -311,11 +272,11 @@ Mặc định, API lỗi sẽ hiện dialog lỗi toàn màn hình. Dùng `ERROR
311
272
  }
312
273
  ```
313
274
 
314
- - `ERROROUT`: map từ XHR error object — các key thông dụng: `status` (HTTP status code), `responseJSON` (parsed error body)
315
- - `ERRORCALLBACK`: action chạy sau khi map ERROROUT (không hiện dialog mặc định)
316
- - Khi có `ERROROUT`, lỗi sẽ KHÔNG tự hiện — bạn phải xử lý hoàn toàn
275
+ `ERROROUT` maps the XHR error (`status` = HTTP code, `responseJSON` = parsed body); `ERRORCALLBACK` runs after.
317
276
 
318
- ### API Method và Header override
277
+ ### METHOD / HEADER
278
+
279
+ Defaults `METHOD = "POST"`, `HEADER = { authorization: token }`; override for GET or external APIs.
319
280
 
320
281
  ```json
321
282
  "getPublicData": {
@@ -326,35 +287,26 @@ Mặc định, API lỗi sẽ hiện dialog lỗi toàn màn hình. Dùng `ERROR
326
287
  }
327
288
  ```
328
289
 
329
- - Mặc định: `METHOD = "POST"`, `HEADER = { authorization: token }`
330
- - Override khi cần GET hoặc custom header cho external API
331
-
332
- ### Literal String Values (Important)
290
+ ### Literal strings in `IN`
333
291
 
334
- Apply this rule only for values passed inside action `IN` mapping.
335
- If a string has no spaces, FUI can treat it like an expression/variable in `IN`.
336
- When you need a plain literal string, force it with:
337
- - Backtick prefix: ``"`myValue"`` (or closed ``"`myValue`"``)
338
- - Single quotes inside JSON string: `"'myValue'"`
292
+ Only for `IN`: no-space strings may be evaluated. Force literal with backtick ``"`myValue"`` (or ``"`myValue`"``) or inner single quotes `"'myValue'"`. Always do this for `id`, `url`, and any key that must stay text.
339
293
 
340
294
  ```json
341
295
  {
342
296
  "FUN": "openWindow",
343
297
  "IN": {
344
- "id": "`winUser",
345
- "url": "'/fp/module?mid=123'",
298
+ "id": "`winUser",
299
+ "url": "'/fp/module?mid=123'",
346
300
  "title": "`Tiêu đề cố định"
347
301
  }
348
302
  }
349
303
  ```
350
304
 
351
- Đặc biệt cần ép literal cho các key như `id`, `url`, hoặc bất kỳ key nào trong `IN` phải giữ nguyên dạng text.
305
+ `attr` props without `:` are already plain strings (`"url": "/fp/module?mid=123"` needs no backtick).
352
306
 
353
- For component props in `attr` without `:`, value is already a plain string prop and usually does not need wrapping — vd `"url": "/fp/module?mid=123"` trong `attr` đã là string literal, không cần backtick.
307
+ ### CONFIRM + CANCEL
354
308
 
355
- ### Confirmation + CANCEL
356
-
357
- `CONFIRM` hiện dialog xác nhận trước khi thực thi. `CANCEL` (optional) chạy khi user bấm Hủy:
309
+ `CONFIRM` (supports `{{ }}`) asks first; optional `CANCEL` runs on Cancel.
358
310
 
359
311
  ```json
360
312
  "deleteItem": {
@@ -366,9 +318,8 @@ For component props in `attr` without `:`, value is already a plain string prop
366
318
  }
367
319
  ```
368
320
 
369
- > `CONFIRM` string hỗ trợ `{{ }}` interpolation từ vueData.
321
+ ### IF / THEN / ELSE
370
322
 
371
- ### Conditional Logic
372
323
  ```json
373
324
  "checkStatus": {
374
325
  "IF": "status === 1",
@@ -381,24 +332,20 @@ For component props in `attr` without `:`, value is already a plain string prop
381
332
  }
382
333
  ```
383
334
 
384
- > Condition: JS expression, truy cập vueData trực tiếp (không cần `vueData.` prefix).
385
- > THEN/ELSE có thể là string JS: `"THEN": "vueData.x = 1"`.
335
+ Condition is JS reading vueData directly (no `vueData.` prefix). THEN/ELSE may be a JS string: `"THEN": "vueData.x = 1"`.
386
336
 
387
337
  ### MESS vs MESSBOX
388
338
 
389
- | | MESS | MESSBOX |
390
- |---|---|---|
391
- | Hiển thị | Toast (góc trên, tự đóng) | Modal dialog (user phải đóng) |
392
- | Dùng khi | Thông báo thành công/thất bại | Cần user đọc và xác nhận đã thấy |
339
+ `MESS` = auto-closing top toast (success/failure). `MESSBOX` = modal the user must close (must be read/acknowledged). Both support `{{ }}`.
393
340
 
394
341
  ```json
395
342
  { "MESS": "Lưu thành công!" }
396
343
  { "MESSBOX": "Phiên của bạn đã hết hạn. Vui lòng đăng nhập lại." }
397
344
  ```
398
345
 
399
- Cả hai hỗ trợ `{{ }}` interpolation.
346
+ ### FUN: showMessage
400
347
 
401
- ### FUN: showMessage — Dialog thông báo có onclose
348
+ `title`; `message` (optional); `onclose` = action after closing (use instead of `MESS` when something must follow).
402
349
 
403
350
  ```json
404
351
  {
@@ -413,13 +360,9 @@ Cả hai hỗ trợ `{{ }}` interpolation.
413
360
  }
414
361
  ```
415
362
 
416
- - `title`: tiêu đề dialog
417
- - `message`: nội dung chi tiết (optional)
418
- - `onclose`: action chạy sau khi user đóng dialog (thay thế `MESS` khi cần làm gì sau khi đọc xong)
419
-
420
- ### CALL — Gọi action đã khai báo
363
+ ### CALL
421
364
 
422
- Action được lưu thành biến trong `data[]` → gọi lại bất cứ đâu bằng `CALL`:
365
+ Actions stored in `data[]` are callable anywhere (CALLBACK, IF/THEN/ELSE, watch):
423
366
 
424
367
  ```json
425
368
  "data": [
@@ -431,29 +374,21 @@ Action được lưu thành biến trong `data[]` → gọi lại bất cứ đ
431
374
  { "CALL": "apiGetLogs" }
432
375
  ```
433
376
 
434
- `CALL` cũng dùng trong CALLBACK, IF/THEN/ELSE, hoặc watch để gọi action đã định nghĩa.
435
-
436
- ---
437
-
438
- ### ARRAY — Trích xuất cột từ mảng cấu trúc
439
-
440
- Tạo mảng phẳng gồm tất cả giá trị của một cột từ array có cấu trúc:
377
+ ### ARRAY + COL
441
378
 
442
379
  ```json
443
380
  {
444
381
  "danhSachID": {
445
382
  "ARRAY": "dsHoaDon",
446
- "COL": "DanhMucThuID"
383
+ "COL": "DanhMucThuID"
447
384
  }
448
385
  }
449
386
  // danhSachID = [32423, 4, 23, 4, 234, 2]
450
387
  ```
451
388
 
452
- ---
453
-
454
- ### OUT — Gán kết quả API vào nhiều biến
389
+ ### OUT object — multiple SELECTs
455
390
 
456
- Thay vì gán toàn bộ response vào một biến, dùng `OUT` dạng object để map từng phần:
391
+ For SPs returning `data[0]`, `data[1]`...; prefer this over CALLBACK + EXE.
457
392
 
458
393
  ```json
459
394
  "getReport": {
@@ -466,19 +401,13 @@ Thay vì gán toàn bộ response vào một biến, dùng `OUT` dạng object
466
401
  }
467
402
  ```
468
403
 
469
- Dùng khi SP trả về nhiều SELECT (`data[0]`, `data[1]`...) — gán mỗi phần vào biến riêng thay vì dùng CALLBACK + EXE.
470
-
471
- ---
472
-
473
- ### MAP — Mapping dữ liệu trong bộ nhớ (không qua API)
474
-
475
- Khi chỉ cần re-map dữ liệu giữa các biến mà không gọi API, dùng block chỉ có `IN` và `OUT`:
404
+ ### MAP — `IN` + `OUT` only, no API
476
405
 
477
406
  ```json
478
407
  {
479
408
  "IN": {
480
409
  "HoTen": "formData.FullName",
481
- "MaSV": "formData.StudentID"
410
+ "MaSV": "formData.StudentID"
482
411
  },
483
412
  "OUT": "mappedData"
484
413
  }
@@ -491,23 +420,19 @@ Khi chỉ cần re-map dữ liệu giữa các biến mà không gọi API, dùn
491
420
  }
492
421
  ```
493
422
 
494
- ---
423
+ ### EXE
495
424
 
496
- ### EXE — Raw JavaScript
425
+ Only for logic action JSON can't express; `vueData` accessible; end with `;`.
497
426
 
498
427
  ```json
499
428
  { "EXE": "vueData.items = vueData.items.filter(x => x.active); " }
500
429
  ```
501
430
 
502
- > Dùng khi cần logic phức tạp không thể diễn tả bằng action JSON. Có thể access `vueData` trực tiếp. Kết thúc bằng `;`.
503
-
504
431
  ---
505
432
 
506
433
  ## 3. Watchers (`watch`)
507
434
 
508
- Trigger actions automatically when data changes.
509
-
510
- For production-grade cascading and filter watcher design, see [watcher-patterns.md](watcher-patterns.md).
435
+ Cascading/filter design: [watcher-patterns.md](watcher-patterns.md).
511
436
 
512
437
  ```json
513
438
  "watch": {
@@ -516,9 +441,7 @@ For production-grade cascading and filter watcher design, see [watcher-patterns.
516
441
  }
517
442
  ```
518
443
 
519
- ### v_old — Giá trị trước khi thay đổi
520
-
521
- Trong CALLBACK hoặc action của watch, `v_old` chứa giá trị CŨ (trước khi thay đổi):
444
+ `v_old` holds the previous value in watch actions/CALLBACK:
522
445
 
523
446
  ```json
524
447
  "watch": {
@@ -529,9 +452,7 @@ Trong CALLBACK hoặc action của watch, `v_old` chứa giá trị CŨ (trướ
529
452
  }
530
453
  ```
531
454
 
532
- ### deep-watch — Watch nested object/array
533
-
534
- Mặc định watch là shallow (chỉ detect thay đổi reference). Dùng `deep-watch` để detect thay đổi bên trong:
455
+ Watch is shallow (reference only). `deep-watch` detects inner changes, runs before normal watches, costs more — use only when needed:
535
456
 
536
457
  ```json
537
458
  "watch": {
@@ -543,19 +464,11 @@ Mặc định watch là shallow (chỉ detect thay đổi reference). Dùng `dee
543
464
  }
544
465
  ```
545
466
 
546
- > `deep-watch` được xử lý trước các watch thông thường. Tốn hiệu năng hơn — chỉ dùng khi cần thiết.
547
-
548
467
  ---
549
468
 
550
- ## 4. Table Cell Action Context
469
+ ## 4. Table cell action context
551
470
 
552
- Khi action chạy từ cell trong `f-table` (t-button, t-check, t-select, t-menu, v-on:click), FUI inject thêm:
553
-
554
- | Variable | Giá trị |
555
- |---|---|
556
- | `item` | Object của row hiện tại |
557
- | `index` | Index của row trong array |
558
- | `$row` | Có thể set thủ công: `{ "$row": "item" }` |
471
+ Actions from `f-table` cells (t-button, t-check, t-select, t-menu, v-on:click) get `item` (row), `index` (row index), and `$row` (settable: `{ "$row": "item" }`). Update one row via `danhSach[index].field` — the standard pattern:
559
472
 
560
473
  ```json
561
474
  {
@@ -579,13 +492,11 @@ Khi action chạy từ cell trong `f-table` (t-button, t-check, t-select, t-menu
579
492
  }
580
493
  ```
581
494
 
582
- > `danhSach[index].kohienthi` — update trực tiếp item trong array bằng `index` inject từ context. Đây là pattern chuẩn để update 1 item cụ thể trong table.
583
-
584
495
  ---
585
496
 
586
- ## 5. URL Parameters → vueData
497
+ ## 5. URL parameters → vueData
587
498
 
588
- URL query params tự động được inject vào `vueData` khi module load:
499
+ On load: `p_router` (path, no query), `p_routers` (segment array), each query param → same-named string variable.
589
500
 
590
501
  ```
591
502
  URL: https://fp.example.vn/ts/newhocsinh?id=12345&mod=abcdef
@@ -595,13 +506,6 @@ URL: https://fp.example.vn/ts/newhocsinh?id=12345&mod=abcdef
595
506
  → vueData.mod = "abcdef"
596
507
  ```
597
508
 
598
- | Biến | Kiểu | Mô tả |
599
- |---|---|---|
600
- | `p_router` | string | Path sau domain, không có query string |
601
- | `p_routers` | array | Mảng các segment của path |
602
- | (query params) | string | Mỗi param trong query string → biến cùng tên |
603
-
604
- Dùng trong `data[]` để đọc URL params:
605
509
  ```json
606
510
  "data": [
607
511
  {
@@ -613,49 +517,36 @@ Dùng trong `data[]` để đọc URL params:
613
517
 
614
518
  ---
615
519
 
616
- ## 6. Controls Layout (`controls`)
617
-
618
- > **Vuetify version (V2 framework):** FUI V2 dùng **Vuetify 1.5** — grid system dùng `v-container / v-layout / v-flex`. FUI V3 (fastproject-3.0.js) dùng **Vuetify 3** với grid `v-container / v-row / v-col`. Phần lớn module.json trong skill này viết theo cú pháp V2 (đại đa số project đang chạy); project mới dùng V3 thì tra bảng ngưỡng `w` V2/V3 bên dưới (mục `w dạng pixel`). Tham chiếu V2: https://v15.vuetifyjs.com/en/framework/grid/ — **đang tạo project mới và phải chọn V2 hay V3?** Mặc định là V3, xem [project-provisioning.md](project-provisioning.md) §Bước 1.
520
+ ## 6. Layout (`controls`)
619
521
 
620
- **MANDATORY**: All UI elements MUST be nested within a Grid System structure: **Container** > **Row** > **Col** > **Element**. Do NOT place elements directly at the root.
522
+ > V2 = Vuetify 1.5 (`v-container / v-layout / v-flex`); V3 (fastproject-3.0.js) = Vuetify 3 (`v-container / v-row / v-col`). Examples use V2 syntax (most projects); for V3 see the `w` thresholds below. V2 ref: https://v15.vuetifyjs.com/en/framework/grid/. New project default is V3 — [project-provisioning.md](project-provisioning.md) §Step 1.
621
523
 
622
- ### Default prop cho Container và Row
524
+ **MANDATORY**: Container > Row > Col > Element. Nothing at root. No `children` key — nest via array `innerHTML`. `attr` = element attributes; `col` = grid-column attributes. Events `v-on:click`, never `@click`.
623
525
 
624
- | Phần | Default | Ghi chú |
625
- |---|---|---|
626
- | Container `prop` | `"fluid grid-list-md"` | Dùng mặc định này trước — chỉ thêm class khác khi có lý do cụ thể |
627
- | Row `prop` | `"row wrap"` hoặc `""` | Dùng mặc định này trước; hàng có nhiều loại control/chiều cao khác nhau dùng `"row wrap align-center"` |
526
+ Defaults — use first, deviate only with reason: container `prop` `"fluid grid-list-md"`; row `prop` `"row wrap"` or `""`, mixed control types/heights `"row wrap align-center"`. Put spacing/alignment on `col` or `attr`, not container/row.
628
527
 
629
- > Spacing và alignment ưu tiên đặt trong `col` hoặc `el attr` hơn là trong container/row prop.
528
+ ### Vertical centering
630
529
 
631
- ### Canh giữa control theo chiều dọc trong cùng một hàng
530
+ Verify each row's controls share a vertical center (esp. mixing input/select, button, icon, checkbox, label, differing heights).
632
531
 
633
- Trước khi chốt một row, phải kiểm tra các control trong row có cùng trục giữa theo chiều dọc hay
634
- không — đặc biệt khi trộn input/select, button, icon, checkbox, label hoặc control có chiều cao khác
635
- nhau.
636
-
637
- - Mặc định cho hàng hỗn hợp: thêm `align-center` vào `prop` của row, ví dụ
638
- `"prop": "row wrap align-center"`.
639
- - Nếu chỉ một control lệch trục và không nên thay alignment của cả hàng, đặt
640
- `"col": { "class": "align-self-center" }` cho cột đó.
641
- - Không dùng `mt-*`, `mb-*` hoặc inline `style` để đẩy từng control lên/xuống nhằm giả lập canh giữa.
642
- - Sau khi dựng layout, tự rà lại row ở desktop và mobile; control xuống dòng phải vẫn có khoảng cách
643
- tự nhiên, không bị lệch vì margin thủ công.
532
+ - Mixed row: add `align-center` to row `prop`.
533
+ - One stray control: `"col": { "class": "align-self-center" }` on it.
534
+ - Never `mt-*`, `mb-*`, or inline `style` to fake centering.
535
+ - Recheck desktop and mobile; wrapped controls keep natural spacing.
644
536
 
645
537
  ```json
646
538
  {
647
539
  "prop": "row wrap align-center",
648
540
  "cols": [
649
541
  { "el": "v-text-field", "w": 6, "attr": { "label": "Từ khóa", "v-model": "vueData.keyword" } },
650
- { "el": "f-button", "w": "", "col": { "class": "shrink" },
651
- "attr": { "label": "Tìm", "icon-text": "mdi-magnify" } }
542
+ { "el": "f-button", "w": "", "col": { "class": "shrink" }, "attr": { "label": "Tìm", "icon-text": "mdi-magnify" } }
652
543
  ]
653
544
  }
654
545
  ```
655
546
 
656
- ### Ngoại lệ — Toàn bộ trang là một custom component (`fluid pa-0`)
547
+ ### Exception — page is one custom component
657
548
 
658
- Khi cả trang chỉ render **một component tự xây dựng duy nhất** (component tự lo toàn bộ layout/padding bên trong), đặt container `prop` thành `"fluid pa-0"` thay vì default `"fluid grid-list-md"` — tránh bị cộng thêm padding mặc định của grid bên ngoài component.
549
+ If the page renders only one self-laid-out component (custom dashboard, `uc-*` app), container `prop` = `"fluid pa-0"` to avoid outer padding. Any other controls on the page (filter, header, sections) → keep `"fluid grid-list-md"`.
659
550
 
660
551
  ```json
661
552
  [
@@ -677,9 +568,8 @@ Khi cả trang chỉ render **một component tự xây dựng duy nhất** (com
677
568
  ]
678
569
  ```
679
570
 
680
- > Chỉ áp dụng khi component con là **toàn bộ nội dung trang** (dashboard tự thiết kế, custom app nhúng qua `uc-*`). Nếu trang còn control khác ngoài component (filter, header, nhiều section) → vẫn dùng default `"fluid grid-list-md"` để giữ spacing chuẩn giữa các phần.
571
+ ### Grid and control object
681
572
 
682
- ### Basic Grid Structure
683
573
  ```json
684
574
  {
685
575
  "prop": "fluid grid-list-md",
@@ -695,25 +585,23 @@ Khi cả trang chỉ render **một component tự xây dựng duy nhất** (com
695
585
  }
696
586
  ```
697
587
 
698
- ### Cấu trúc đầy đủ của một element trong `cols`
699
-
700
588
  ```json
701
589
  {
702
- "w": "6",
703
- "el": "v-text-field",
704
- "col": { "class": "py-0" },
705
- "attr": { "label": "Tên", "v-model": "tenBien" },
590
+ "w": "6",
591
+ "el": "v-text-field",
592
+ "col": { "class": "py-0" },
593
+ "attr": { "label": "Tên", "v-model": "tenBien" },
706
594
  "innerHTML": "..."
707
595
  }
708
596
  ```
709
597
 
710
- | Field | Truyền vào | Mô tả |
711
- |---|---|---|
712
- | `w` | `v-flex` | Shorthand width — `1`–`12` (grid) hoặc `>= 25` (pixel, **bắt buộc kèm `col: { "class": "shrink" }`** — xem mục dưới). **BẮT BUỘC với mọi item trong `cols`** |
713
- | `col` | `v-flex` | Các thuộc tính khác của v-flex: class, style, offset, align-self... |
714
- | `el` | tag component | Tag HTML (`div`, `span`), component Vuetify hoặc FUI (`v-text-field`, `f-button`, `uc-*`...) |
715
- | `attr` | element | Props/attrs/directive của chính component: `label`, `v-model`, `:disabled`, `v-on:click`. **KHÔNG dùng `@`** |
716
- | `innerHTML` | slot default | HTML string hoặc **mảng đệ quy** các Control Object lồng bên trong |
598
+ | Field | To | Meaning |
599
+ | ----------- | ------------ | ------------------------------------------------------------------------------------------------------------- |
600
+ | `w` | `v-flex` | **Required on every `cols` item.** `1`–`12` grid or `>= 25` px (px **requires `col: { "class": "shrink" }`**) |
601
+ | `col` | `v-flex` | class, style, offset, align-self... |
602
+ | `el` | tag | `div`, `span`, Vuetify/FUI (`v-text-field`, `f-button`, `uc-*`...) |
603
+ | `attr` | element | props/attrs/directives (`label`, `v-model`, `:disabled`, `v-on:click`); **no `@`** |
604
+ | `innerHTML` | default slot | HTML string (supports `{{ }}`) or recursive array of Control Objects |
717
605
 
718
606
  ```json
719
607
  {
@@ -729,108 +617,74 @@ Khi cả trang chỉ render **một component tự xây dựng duy nhất** (com
729
617
  }
730
618
  ```
731
619
 
732
- ### `w` dạng pixel — BẮT BUỘC kèm `col: { "class": "shrink" }`
733
-
734
- `w` có **hai chế độ**, runtime chọn theo ngưỡng chứ không theo ý định của người viết:
620
+ ```json
621
+ { "el": "div", "w": "12", "innerHTML": "Xin chào {{formData.fullName}}" }
622
+ ```
735
623
 
736
- - `w` nhỏ → **số cột grid**, sinh class `sm{w}` (V2) / `v-col-{w}` (V3)
737
- - `w` lớn → **bề rộng cố định px**, sinh inline `flex-basis: {w}px`
624
+ ### Pixel `w` — REQUIRES `col: { "class": "shrink" }`
738
625
 
739
- Nhưng cột grid mặc định là `flex-grow: 1` (`.flex` ở V2, `.v-col` ở V3), mà **`flex-grow` thắng `flex-basis`**: cột vẫn giãn ra chiếm hết chỗ trống, con số px **không có tác dụng nào**. Không lỗi JS, không `[Vue warn]` — chỉ là bề rộng khác điều mình khai.
626
+ Small `w` → grid class `sm{w}` (V2) / `v-col-{w}` (V3). Large `w` → inline `flex-basis: {w}px`. But grid columns have `flex-grow: 1` (`.flex`/`.v-col`), which beats `flex-basis`: without `shrink` the px value silently does nothing.
740
627
 
741
628
  ```json
742
- // ❌ SAI — 300 hoàn toàn vô tác dụng, cột vẫn giãn full
629
+ // ❌ WRONG — 300 has no effect, column still stretches full
743
630
  { "el": "v-text-field", "w": 300, "attr": { "v-model": "tuKhoa" } }
744
631
 
745
- // ✅ ĐÚNG — shrink tắt flex-grow, flex-basis 300px mới ăn
632
+ // ✅ RIGHT — shrink disables flex-grow, flex-basis 300px applies
746
633
  { "el": "v-text-field", "w": 300, "col": { "class": "shrink" },
747
634
  "attr": { "v-model": "tuKhoa" } }
748
635
  ```
749
636
 
750
- Số đo thật (chromium, framework V2 + V3 thật, viewport 1200px, cột kế bên chiếm phần còn lại):
637
+ Measured (chromium, 1200px, neighbour takes rest):
751
638
 
752
- | Khai | V2 | V3 |
753
- |---|---|---|
754
- | `w: 300` không có gì thêm | 687px ❌ | 738px ❌ |
755
- | `w: 300` + `col: { "shrink": true }` | 300px ✅ | 738px ❌ |
756
- | `w: 300` + `col: { "class": "shrink" }` | 300px ✅ | 300px ✅ |
639
+ | Declared | V2 | V3 |
640
+ | ------------------------------ | -------- | -------- |
641
+ | `w: 300` | 687px ❌ | 738px ❌ |
642
+ | + `col: { "shrink": true }` | 300px ✅ | 738px ❌ |
643
+ | + `col: { "class": "shrink" }` | 300px ✅ | 300px ✅ |
757
644
 
758
- **Chỉ `col: { "class": "shrink" }` đúng ở cả hai framework.** Dạng thuộc tính trần `col: { "shrink": true }` là prop của `v-flex` (FUI V2 = Vuetify 1.5, xem §Vuetify version ở trên); Vuetify 3 không có prop đó nên nó rơi xuống thành attribute DOM trơ. Class `shrink` thì ở V3 do chính `projectdefaultstyle.css` của FUI định nghĩa (`flex-grow:0; flex-shrink:1; flex-basis:content`), nên có ở cả hai. `col: { "class": "flex-grow-0" }` cũng cho 300px ở cả hai — dùng `shrink` cho thống nhất với phần còn lại của skill.
645
+ Only `col: { "class": "shrink" }` works in both: `shrink: true` is a V2 `v-flex` prop, inert in Vuetify 3; the `shrink` class is defined for V3 in FUI's `projectdefaultstyle.css` (`flex-grow:0; flex-shrink:1; flex-basis:content`). `"flex-grow-0"` also works; use `shrink` for consistency.
759
646
 
760
- #### Ngưỡng khác nhau giữa V2 và V3 → tránh hẳn `w` 13–24
647
+ **Never `w` 13–24.** V2 (`fastproject.js`) switches to px at `w > 12`; V3 (`fastproject-3.0.js`) at `w > 24`, so V3 `w: 20` emits `v-col-20`, which doesn't exist (only `.v-col-1`…`.v-col-12`) → no width, equal split (measured `flex-basis: 0px`, 600px). Rule: `w` is `1`–`12` or `>= 25` + shrink. `fui module validate` warns on both (including `cols` nested in `innerHTML`) — run it before publishing.
761
648
 
762
- | | Ngưỡng chuyển sang px | `w` = 20 cho ra |
763
- |---|---|---|
764
- | V2 (`fastproject.js`) | `w > 12` | `flex-basis: 20px` — px, hoạt động |
765
- | V3 (`fastproject-3.0.js`) | `w > 24` | class `v-col-20` — **Vuetify 3 chỉ định nghĩa `.v-col-1` … `.v-col-12`** |
649
+ ### Toolbar row — every column `shrink` except `v-spacer`
766
650
 
767
- Ở V3, `w` từ 13 đến 24 sinh ra một class **không tồn tại**: cột không có bề rộng nào cả, rơi về chia đều (đo được: `w: 20` → `flex-basis: 0px`, rộng 600px). Đây là vùng chết — cùng một `w` cho hai kết quả trái ngược trên hai framework.
768
-
769
- > **Luật:** `w` chỉ dùng `1`–`12` (chia cột) hoặc **`>= 25`** (pixel, kèm `col: { "class": "shrink" }`). Không bao giờ dùng 13–24.
770
-
771
- `module_validate` kiểm cả hai vế này (warning, quét cả `cols` lồng trong `innerHTML`) — chạy trước khi publish thì không phải phát hiện qua ảnh render.
772
-
773
- ### Hàng nhiều control (toolbar) — CHUẨN MỰC: mọi cột `shrink`, trừ `v-spacer`
774
-
775
- Đây là hàng phổ biến nhất trong FUI: vài filter + nút thao tác bên trái, một nhóm nút đẩy sang phải. **Khuôn chuẩn — chép nguyên rồi thay control:**
651
+ Canonical template (filters + buttons left, group pushed right):
776
652
 
777
653
  ```json
778
654
  {
779
655
  "prop": "row",
780
656
  "cols": [
781
- { "el": "v-select", "w": "150", "col": "shrink", "attr": {} },
782
- { "el": "v-select", "w": "180", "col": "shrink", "attr": {} },
783
- { "el": "f-button", "w": "", "col": "shrink", "attr": {} },
784
- { "el": "v-spacer", "w": "" },
785
- { "el": "f-excel-reader", "col": "shrink", "attr": {} },
786
- { "el": "f-button", "w": "", "col": "shrink", "attr": {} }
657
+ { "el": "v-select", "w": "150", "col": "shrink", "attr": {} },
658
+ { "el": "v-select", "w": "180", "col": "shrink", "attr": {} },
659
+ { "el": "f-button", "w": "", "col": "shrink", "attr": {} },
660
+ { "el": "v-spacer", "w": "" },
661
+ { "el": "f-excel-reader", "col": "shrink", "attr": {} },
662
+ { "el": "f-button", "w": "", "col": "shrink", "attr": {} }
787
663
  ]
788
664
  }
789
665
  ```
790
666
 
791
- > **Luật:** trong một hàng chứa nhiều control điều khiển, **MỌI cột đều `"col": "shrink"`** — kể cả cột `w` rỗng. Ngoại lệ **duy nhất** là `v-spacer`: nó phải giãn được thì mới đẩy được nhóm sau sang phải, cho nó `shrink` là vô hiệu hoá chính nó.
792
-
793
- **`w` rỗng KHÔNG có nghĩa là "tự co theo nội dung".** `fastproject*.js` bọc toàn bộ phần xử lý bề rộng trong `if (c.w)`, nên `w: ""` sinh ra **không class, cũng không `flex-basis`** — cột còn trơ là `.v-col` / `.flex` với `flex-grow: 1`, tức **chia đều phần trống** y như mọi cột khác. Nút bị kéo dãn ra, các nút cách nhau những khoảng trống bằng nhau, và nhóm cuối hàng không sát mép phải.
794
-
795
- Số đo thật (chromium, framework V2 + V3 thật, viewport 1200px; hàng gồm 2 select `w:150`/`w:180` + nút "Tìm" | `v-spacer` | 2 nút cuối):
796
-
797
- | | select `w:150` | nút "Tìm" | `v-spacer` | Kết quả nhìn thấy |
798
- |---|---|---|---|---|
799
- | Không cột nào `shrink` | 271 / 240 ❌ | 145 / 154 | 145 / 90 | filter phình, mọi thứ dàn đều |
800
- | **Chỉ `v-spacer`, các cột khác không `shrink`** | 150 / 150 | **218 / 198** ❌ | 218 / 134 | **nút phình, hở khoảng giữa các nút, nhóm cuối không sát phải** |
801
- | `shrink` cả `v-spacer` | 150 / 150 | 88 / 64 | **24 / 0** ❌ | cả hàng dồn về trái, mất hẳn canh phải (Σ 780/662 trên 1200px) |
802
- | **`shrink` mọi cột trừ `v-spacer`** | 150 / 150 ✅ | 88 / 64 ✅ | **445 / 537** ✅ | filter đúng px, nút vừa chữ, nhóm cuối sát mép phải |
803
-
804
- *(số bên trái = V3, bên phải = V2)*
667
+ **Rule:** every control column gets `"col": "shrink"`, including empty-`w` ones. Sole exception: `v-spacer` (shrinking it disables it). Empty `w` ≠ fit content: width logic is inside `if (c.w)`, so `w: ""` emits nothing and the column (`flex-grow: 1`) takes an equal share of free space. Any non-`shrink` column gets a share; in a toolbar only `v-spacer` should.
805
668
 
806
- Hàng thứ hai chính là lỗi hay gặp nhất: nghe **"cho hai nút này canh phải"** thì chỉ chèn `v-spacer` mà quên `shrink`. `v-spacer` lúc đó cũng chỉ là một cột `flex-grow: 1` ngang hàng với các nút — nó không "đẩy" được gì cả, chỉ nhận đúng một phần bằng nhau. Muốn hai nút **sát nhau và sát mép phải** thì hai nút đó **bắt buộc** phải hết giãn, nghĩa là phải có `shrink`.
669
+ Measured (1200px; 2 selects `w:150`/`w:180` + "Tìm" | `v-spacer` | 2 buttons; V3 / V2):
807
670
 
808
- Ghi nhớ theo hệ quả chứ không theo quy tắc rời: **cột nào không `shrink` là cột được chia phần trống.** Trong một hàng toolbar, thứ duy nhất đáng được chia phần trống là khoảng trắng — tức `v-spacer`.
671
+ | | select `w:150` | "Tìm" | `v-spacer` | Result |
672
+ | -------------------------------- | -------------- | ---------------- | ---------------- | ---------------------------------------------- |
673
+ | no `shrink` | 271 / 240 ❌ | 145 / 154 | 145 / 90 | filters bloat, evenly spread |
674
+ | **`v-spacer` only, no `shrink`** | 150 / 150 | **218 / 198** ❌ | 218 / 134 | buttons bloat, gaps, end group not flush right |
675
+ | `shrink` incl. `v-spacer` | 150 / 150 | 88 / 64 | **24 / 0** ❌ | row packs left (Σ 780/662 of 1200px) |
676
+ | **`shrink` all but `v-spacer`** | 150 / 150 ✅ | 88 / 64 ✅ | **445 / 537** ✅ | exact px, buttons fit, end group flush right |
809
677
 
810
- Vài chi tiết đi kèm:
811
-
812
- - `col` viết được **chuỗi ngắn** (`"col": "shrink"`) hoặc **object** (`"col": { "class": "shrink" }`) — tương đương nhau; chuỗi ngắn gọn hơn cho toolbar. Cần thêm thứ khác (`v-if`, `style`…) thì dùng object và giữ `class: "shrink"` bên trong.
813
- - **Không bao giờ là `"col": { "shrink": true }`** — dạng thuộc tính trần chỉ ăn ở V2 (xem mục trên).
814
- - Cột **không** có `v-spacer` mà muốn canh phải cả hàng: đặt `prop` của row là `"row justify-end"` thay vì chèn spacer.
815
- - Cần nhiều hơn một nhóm (trái | giữa | phải): chèn **hai** `v-spacer`, mỗi cái vẫn không `shrink`.
816
- - Hàng **form nhập liệu** thì ngược lại — ở đó các field *nên* chia cột (`w` 1–12, không `shrink`) để co giãn theo màn hình. `shrink` là chuẩn cho **hàng điều khiển**, không phải cho mọi hàng.
817
-
818
- ### `innerHTML` Supports `{{ }}`
819
-
820
- `innerHTML` dạng string hỗ trợ Vue template interpolation để bind text reactive:
821
-
822
- ```json
823
- { "el": "div", "w": "12", "innerHTML": "Xin chào {{formData.fullName}}" }
824
- ```
678
+ Row 2 is the common bug ("right-align these buttons" → spacer added, `shrink` forgotten).
825
679
 
826
- ### Layout Rules — 4 quy tắc cứng
680
+ - `"col": "shrink"` ≡ `"col": { "class": "shrink" }`; use the object when adding `v-if`, `style`…, keeping `class: "shrink"`.
681
+ - Never `"col": { "shrink": true }` (V2 only).
682
+ - Right-align a whole row without spacer: row `prop` `"row justify-end"`.
683
+ - Left | center | right: two `v-spacer`s, neither `shrink`.
684
+ - Data-entry form rows are the opposite: fields use `w` 1–12, no `shrink`. `shrink` is for control rows only.
827
685
 
828
- - **Wrapper**: LUÔN bắt đầu bằng Grid Wrapper (`container > rows > cols`), không đặt element ở gốc.
829
- - **Không có key `children`**: lồng control bằng `innerHTML` dạng mảng.
830
- - **Attributes**: `attr` cho thuộc tính của element; `col` cho thuộc tính của cột grid.
831
- - **Events**: dùng `v-on:click` (JSON hợp lệ), không dùng `@click`.
686
+ ### Responsive
832
687
 
833
- ### Responsive Forms
834
688
  ```json
835
689
  "attr": {
836
690
  ":width": "$vuetify.breakpoint.mdAndUp ? '500px' : '100%'"
@@ -839,9 +693,9 @@ Vài chi tiết đi kèm:
839
693
 
840
694
  ---
841
695
 
842
- ## 7. Common Element Patterns
696
+ ## 7. Common element patterns
843
697
 
844
- - **HTML Content**: Use `innerHTML` for text or simple HTML.
845
- - **Events**: `v-on:click`, `v-on:change`. wrap logic in `CALL()`.
846
- - **Visibility**: Use `v-if` (pre-render) or `v-show` (CSS toggle).
847
- - **Loops**: `v-for` is rarely used directly in `module.json`. Use `f-table` for lists or recursive partials.
698
+ - Text/simple HTML: `innerHTML`.
699
+ - Events: `v-on:click`, `v-on:change`; wrap logic in `CALL()`.
700
+ - Visibility: `v-if` (pre-render) or `v-show` (CSS toggle).
701
+ - Lists: avoid raw `v-for`; use `f-table` or recursive partials.