@fui-org/fui-cli 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/fui-x7kph78r.js +420 -0
  2. package/dist/fui.js +1 -429
  3. package/package.json +3 -3
  4. package/skills/fui/SKILL.md +3 -3
  5. package/skills/fui-skill-cli/SKILL.md +139 -0
  6. package/skills/fui-skill-cli/references/INDEX.md +110 -0
  7. package/skills/fui-skill-cli/references/advanced-techniques.md +168 -0
  8. package/skills/fui-skill-cli/references/coding-standards.md +112 -0
  9. package/skills/fui-skill-cli/references/component-design.md +448 -0
  10. package/skills/fui-skill-cli/references/component-quickref.md +78 -0
  11. package/skills/fui-skill-cli/references/component-table.md +248 -0
  12. package/skills/fui-skill-cli/references/components-dialog.md +191 -0
  13. package/skills/fui-skill-cli/references/components-display.md +141 -0
  14. package/skills/fui-skill-cli/references/components-echart.md +316 -0
  15. package/skills/fui-skill-cli/references/components-input.md +335 -0
  16. package/skills/fui-skill-cli/references/controls-patterns.md +701 -0
  17. package/skills/fui-skill-cli/references/controls-styling-vocabulary.md +137 -0
  18. package/skills/fui-skill-cli/references/db-table-design.md +73 -0
  19. package/skills/fui-skill-cli/references/db-workflow.md +288 -0
  20. package/skills/fui-skill-cli/references/default-function.md +425 -0
  21. package/skills/fui-skill-cli/references/design-modes.md +57 -0
  22. package/skills/fui-skill-cli/references/echart-templates.md +489 -0
  23. package/skills/fui-skill-cli/references/fastproject.md +99 -0
  24. package/skills/fui-skill-cli/references/fsheet.md +203 -0
  25. package/skills/fui-skill-cli/references/fullstack-workflow.md +313 -0
  26. package/skills/fui-skill-cli/references/module-data-patterns.md +117 -0
  27. package/skills/fui-skill-cli/references/module-json-anatomy.md +132 -0
  28. package/skills/fui-skill-cli/references/module-structure.md +141 -0
  29. package/skills/fui-skill-cli/references/new-session.md +85 -0
  30. package/skills/fui-skill-cli/references/pdfmake.md +60 -0
  31. package/skills/fui-skill-cli/references/permission-system.md +150 -0
  32. package/skills/fui-skill-cli/references/platform-architecture.md +269 -0
  33. package/skills/fui-skill-cli/references/project-config.md +303 -0
  34. package/skills/fui-skill-cli/references/project-provisioning.md +278 -0
  35. package/skills/fui-skill-cli/references/script-map.md +262 -0
  36. package/skills/fui-skill-cli/references/sql-clr-functions.md +225 -0
  37. package/skills/fui-skill-cli/references/system-design.md +89 -0
  38. package/skills/fui-skill-cli/references/tapi-file-api.md +185 -0
  39. package/skills/fui-skill-cli/references/tapi-permission-patterns.md +156 -0
  40. package/skills/fui-skill-cli/references/tapi-reference.md +474 -0
  41. package/skills/fui-skill-cli/references/tools-registry.md +84 -0
  42. package/skills/fui-skill-cli/references/ui-crosswindow-patterns.md +321 -0
  43. package/skills/fui-skill-cli/references/ui-dialog-patterns.md +255 -0
  44. package/skills/fui-skill-cli/references/ui-layout-patterns.md +176 -0
  45. package/skills/fui-skill-cli/references/ui-patterns.md +303 -0
  46. package/skills/fui-skill-cli/references/ui-screenshot-review.md +95 -0
  47. package/skills/fui-skill-cli/references/ui-table-cell-patterns.md +318 -0
  48. package/skills/fui-skill-cli/references/ui-templates.md +22 -0
  49. package/skills/fui-skill-cli/references/verification.md +236 -0
  50. package/skills/fui-skill-cli/references/watcher-patterns.md +163 -0
  51. package/skills/fui-skill-cli/references/websocket-realtime.md +271 -0
@@ -0,0 +1,701 @@
1
+ # Controls Patterns & Logic
2
+
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`.)_
4
+
5
+ ---
6
+
7
+ ## Prefer `f-*` over `v-*`
8
+
9
+ Use `v-*` only when no `f-*` equivalent exists (check [component-quickref.md](component-quickref.md)); `f-*` has built-in features and runtime integration.
10
+
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 |
19
+
20
+ Defaults auto-injected (`defaultControlAttr` in `scripts/component.js`) — declare a prop only to override:
21
+
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` |
29
+
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`).
31
+
32
+ ---
33
+
34
+ ## 0. Value Resolution Rules (Fundamental)
35
+
36
+ `getVueData(value, src)` resolves every string in `data[]` and `IN`:
37
+
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
+ | `""` | `""` |
46
+
47
+ ```json
48
+ "data": [
49
+ {
50
+ "a": "Hang So", // has space → literal string "Hang So"
51
+ "b": 3, // number → 3
52
+ "c": true, // boolean → true
53
+ "d": null, // null → null
54
+ "e": "val1", // no space → JS eval: value of variable val1
55
+ "f": "val4.prop2", // JS eval: vueData.val4.prop2
56
+ "g": "val5[1]", // JS eval: 2nd element of array val5
57
+ "h": "[val1]", // JS eval: array [value_of_val1]
58
+ "i": "[1,2,3].map(x=>x*2)", // no space → JS eval → [2,4,6]
59
+ "j": "`Xin chào {{user.UserName}}", // backtick → lodash template → "Xin chào Tuấn"
60
+ "k": "tổng là {{a+b}} item", // {{ }} → lodash template → "tổng là 3 item"
61
+ "l": "tuimuonghep{{val4.prop3}}chuoi" // {{ }} → "tuimuonghep10chuoi"
62
+ }
63
+ ]
64
+ ```
65
+
66
+ `"val1"` = value of `val1`, not the string; literal needs ``"`val1"``.
67
+
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.
69
+
70
+ `{{ }}` takes any JS expression:
71
+
72
+ ```json
73
+ "label": "Còn {{items.length}} bản ghi",
74
+ "title": "Ngày {{moment().format('DD/MM/YYYY')}}",
75
+ "flag": "Là array: {{Array.isArray(myList)}}"
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 1. `data[]` keys and assignment
81
+
82
+ ### Key notation
83
+
84
+ ```json
85
+ "data": [
86
+ {
87
+ "val": "Hang So", // simple variable
88
+ "obj.prop": "val1", // dot notation → vueData.obj.prop
89
+ "obj.nested.deep": true, // multi-level dot
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
95
+ }
96
+ ]
97
+ ```
98
+
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).
100
+
101
+ ### Events/slots in `attr` — full form only
102
+
103
+ Every event/slot of every component, incl. custom `f-*` and `$emit()` events:
104
+
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` |
109
+
110
+ ```json
111
+ // ❌ WRONG — shorthand, fails to load
112
+ { "el": "f-contest-list", "attr": { "@saved": "CALL(apiLoadContests)" } }
113
+
114
+ // ✅ RIGHT
115
+ { "el": "f-contest-list", "attr": { "v-on:saved": "CALL(apiLoadContests)" } }
116
+ ```
117
+
118
+ `#PARENT.field` / `#Win1.field` in `data[]` is FUI cross-window syntax, unrelated to Vue slot `#`.
119
+
120
+ ### Alias and JS init
121
+
122
+ ```json
123
+ "GanLienGiaTri": "val1" // GanLienGiaTri = current value of val1
124
+ ```
125
+
126
+ Snapshot at `data[]` run, not reactive — use `watch` for reactivity.
127
+
128
+ ```json
129
+ "data": [
130
+ {
131
+ "today": "moment().format('YYYY-MM-DD')",
132
+ "ids": "[1,2,3,4].map(x=>x*2)",
133
+ "firstItem": "dsUser[0]",
134
+ "total": "dsItems.length"
135
+ }
136
+ ]
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 2. Actions (`data`)
142
+
143
+ Declare named action objects in `data`; invoke with `CALL`.
144
+
145
+ ### CORE RULE — one `{}` = ONE command; sequence = ARRAY `[]`
146
+
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.
148
+
149
+ ```json
150
+ // ❌ WRONG — only CALL runs. EXE and MESS NEVER run.
151
+ "CALLBACK": {
152
+ "EXE": "vueData.previewRows = []; vueData.excelRaw = [];",
153
+ "CALL": "getList",
154
+ "MESS": "Đã cập nhật dữ liệu tháng"
155
+ }
156
+ ```
157
+
158
+ ```json
159
+ // ✅ RIGHT — three commands, three objects, run in written order.
160
+ "CALLBACK": [
161
+ { "EXE": "vueData.previewRows = []; vueData.excelRaw = [];" },
162
+ { "CALL": "getList" },
163
+ { "MESS": "Đã cập nhật dữ liệu tháng" }
164
+ ]
165
+ ```
166
+
167
+ `vueAction()` (`fastproject.js:407`, `fastproject-3.0.js:448`, same in V2/V3) checks keys in fixed order, each branch returns:
168
+
169
+ ```
170
+ IN (tham số, không return) → API ⏎ → CALL ⏎ → EXE ⏎ → FUN ⏎ → IF ⏎ → MESS ⏎ → MESSBOX ⏎ → (fallthrough) mapData
171
+ ```
172
+
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".
174
+
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.
176
+
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`) |
181
+
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`).
183
+
184
+ Assignment fragments (`{ "dlgOpen": false, "form.Name": "" }`) run only in the fallthrough branch; any command in the same object silently drops them:
185
+
186
+ ```json
187
+ // ❌ WRONG — CALL runs, the three variables are NOT assigned
188
+ { "PageNum": 1, "Description": "", "IsPaper": false, "CALL": "reload" }
189
+
190
+ // ✅ RIGHT
191
+ [
192
+ { "PageNum": 1, "Description": "", "IsPaper": false },
193
+ { "CALL": "reload" }
194
+ ]
195
+ ```
196
+
197
+ ### Keys
198
+
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 |
213
+
214
+ ```json
215
+ "fetchUsers": {
216
+ "API": "/api/users",
217
+ "IN": { "GroupID": "vueData.selectedGroup" },
218
+ "OUT": "userList",
219
+ "CALLBACK": { "MESS": "Loaded!" }
220
+ }
221
+ ```
222
+
223
+ ```json
224
+ "apiLoadData": {
225
+ "API": "/api/controller/action",
226
+ "IN": {
227
+ "Page": 1,
228
+ "Search": "vueData.searchText",
229
+ "ID": "item.ID"
230
+ },
231
+ "OUT": "items",
232
+ "CALLBACK": [
233
+ { "MESS": "Loaded!" },
234
+ { "CALL": "anotherAction" }
235
+ ]
236
+ }
237
+ ```
238
+
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.
241
+
242
+ ### URL interpolation
243
+
244
+ `{{ }}` in `API` reads vueData:
245
+
246
+ ```json
247
+ "getDetail": {
248
+ "API": "/ts/Student/{{StudentID}}",
249
+ "OUT": "studentDetail"
250
+ },
251
+ "getReport": {
252
+ "API": "/ts/Report/{{sYear}}/{{sMonth}}",
253
+ "OUT": "reportData"
254
+ }
255
+ ```
256
+
257
+ ### ERROROUT / ERRORCALLBACK
258
+
259
+ Default: full-screen error dialog. With `ERROROUT` nothing shows automatically — you handle it.
260
+
261
+ ```json
262
+ "saveData": {
263
+ "API": "/ts/Student_Insert",
264
+ "IN": { "Name": "sName" },
265
+ "ERROROUT": {
266
+ "errStatus": "status",
267
+ "errMessage": "responseJSON"
268
+ },
269
+ "ERRORCALLBACK": {
270
+ "MESS": "Lưu thất bại: {{errMessage.Message}}"
271
+ }
272
+ }
273
+ ```
274
+
275
+ `ERROROUT` maps the XHR error (`status` = HTTP code, `responseJSON` = parsed body); `ERRORCALLBACK` runs after.
276
+
277
+ ### METHOD / HEADER
278
+
279
+ Defaults `METHOD = "POST"`, `HEADER = { authorization: token }`; override for GET or external APIs.
280
+
281
+ ```json
282
+ "getPublicData": {
283
+ "API": "/ts/PublicData",
284
+ "METHOD": "GET",
285
+ "HEADER": { "x-api-key": "`my-api-key" },
286
+ "OUT": "publicData"
287
+ }
288
+ ```
289
+
290
+ ### Literal strings in `IN`
291
+
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.
293
+
294
+ ```json
295
+ {
296
+ "FUN": "openWindow",
297
+ "IN": {
298
+ "id": "`winUser",
299
+ "url": "'/fp/module?mid=123'",
300
+ "title": "`Tiêu đề cố định"
301
+ }
302
+ }
303
+ ```
304
+
305
+ `attr` props without `:` are already plain strings (`"url": "/fp/module?mid=123"` needs no backtick).
306
+
307
+ ### CONFIRM + CANCEL
308
+
309
+ `CONFIRM` (supports `{{ }}`) asks first; optional `CANCEL` runs on Cancel.
310
+
311
+ ```json
312
+ "deleteItem": {
313
+ "CONFIRM": "Bạn có chắc muốn xóa {{dongChon.Name}}?",
314
+ "API": "/api/delete",
315
+ "IN": { "id": "dongChon.ID" },
316
+ "CALLBACK": { "CALL": "reloadData" },
317
+ "CANCEL": { "MESS": "Đã hủy thao tác" }
318
+ }
319
+ ```
320
+
321
+ ### IF / THEN / ELSE
322
+
323
+ ```json
324
+ "checkStatus": {
325
+ "IF": "status === 1",
326
+ "THEN": { "MESS": "Active" },
327
+ "ELSE": {
328
+ "IF": "status === 2",
329
+ "THEN": { "MESS": "Pending" },
330
+ "ELSE": { "MESS": "Inactive" }
331
+ }
332
+ }
333
+ ```
334
+
335
+ Condition is JS reading vueData directly (no `vueData.` prefix). THEN/ELSE may be a JS string: `"THEN": "vueData.x = 1"`.
336
+
337
+ ### MESS vs MESSBOX
338
+
339
+ `MESS` = auto-closing top toast (success/failure). `MESSBOX` = modal the user must close (must be read/acknowledged). Both support `{{ }}`.
340
+
341
+ ```json
342
+ { "MESS": "Lưu thành công!" }
343
+ { "MESSBOX": "Phiên của bạn đã hết hạn. Vui lòng đăng nhập lại." }
344
+ ```
345
+
346
+ ### FUN: showMessage
347
+
348
+ `title`; `message` (optional); `onclose` = action after closing (use instead of `MESS` when something must follow).
349
+
350
+ ```json
351
+ {
352
+ "FUN": "showMessage",
353
+ "IN": {
354
+ "title": "`Xác nhận thành công",
355
+ "message": "`Dữ liệu đã được lưu. Tiếp tục?",
356
+ "onclose": {
357
+ "CALL": "reloadData"
358
+ }
359
+ }
360
+ }
361
+ ```
362
+
363
+ ### CALL
364
+
365
+ Actions stored in `data[]` are callable anywhere (CALLBACK, IF/THEN/ELSE, watch):
366
+
367
+ ```json
368
+ "data": [
369
+ { "apiGetLogs": { "API": "/nema/Wifi_Log_today", "OUT": "dataTableLogs" } }
370
+ ]
371
+ ```
372
+
373
+ ```json
374
+ { "CALL": "apiGetLogs" }
375
+ ```
376
+
377
+ ### ARRAY + COL
378
+
379
+ ```json
380
+ {
381
+ "danhSachID": {
382
+ "ARRAY": "dsHoaDon",
383
+ "COL": "DanhMucThuID"
384
+ }
385
+ }
386
+ // danhSachID = [32423, 4, 23, 4, 234, 2]
387
+ ```
388
+
389
+ ### OUT object — multiple SELECTs
390
+
391
+ For SPs returning `data[0]`, `data[1]`...; prefer this over CALLBACK + EXE.
392
+
393
+ ```json
394
+ "getReport": {
395
+ "API": "/ts/TS_Report",
396
+ "OUT": {
397
+ "tongSoLuong": "data[0][0].Total",
398
+ "danhSachChiTiet": "data[1]",
399
+ "thongKe": "data[2]"
400
+ }
401
+ }
402
+ ```
403
+
404
+ ### MAP — `IN` + `OUT` only, no API
405
+
406
+ ```json
407
+ {
408
+ "IN": {
409
+ "HoTen": "formData.FullName",
410
+ "MaSV": "formData.StudentID"
411
+ },
412
+ "OUT": "mappedData"
413
+ }
414
+ ```
415
+
416
+ ```json
417
+ {
418
+ "IN": { "HoTen": "formData.FullName" },
419
+ "OUT": { "displayName": "HoTen" }
420
+ }
421
+ ```
422
+
423
+ ### EXE
424
+
425
+ Only for logic action JSON can't express; `vueData` accessible; end with `;`.
426
+
427
+ ```json
428
+ { "EXE": "vueData.items = vueData.items.filter(x => x.active); " }
429
+ ```
430
+
431
+ ---
432
+
433
+ ## 3. Watchers (`watch`)
434
+
435
+ Cascading/filter design: [watcher-patterns.md](watcher-patterns.md).
436
+
437
+ ```json
438
+ "watch": {
439
+ "searchText": { "CALL": "apiLoadData" },
440
+ "selectedGroup": { "CALL": "apiLoadUsers" }
441
+ }
442
+ ```
443
+
444
+ `v_old` holds the previous value in watch actions/CALLBACK:
445
+
446
+ ```json
447
+ "watch": {
448
+ "taoID": {
449
+ "backupID": "v_old.taoID",
450
+ "BAC": 12345
451
+ }
452
+ }
453
+ ```
454
+
455
+ Watch is shallow (reference only). `deep-watch` detects inner changes, runs before normal watches, costs more — use only when needed:
456
+
457
+ ```json
458
+ "watch": {
459
+ "deep-watch": {
460
+ "formData": { "CALL": "autoSave" },
461
+ "selectedItems": { "CALL": "recalcTotal" }
462
+ },
463
+ "sModuleID": { "CALL": "reload" }
464
+ }
465
+ ```
466
+
467
+ ---
468
+
469
+ ## 4. Table cell action context
470
+
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:
472
+
473
+ ```json
474
+ {
475
+ "el": "t-button",
476
+ "text": "Tên",
477
+ "value": "ten",
478
+ "attr": {
479
+ ":label": "`${item.ten}-${vueData.val1}`",
480
+ ":action": [
481
+ { "$row": "item" },
482
+ {
483
+ "API": "/api/getDetail",
484
+ "OUT": "DETAIL",
485
+ "CALLBACK": {
486
+ "danhSach[index].kohienthi": true
487
+ }
488
+ },
489
+ { "MESS": "Row {{danhSach[index].ten}}" }
490
+ ]
491
+ }
492
+ }
493
+ ```
494
+
495
+ ---
496
+
497
+ ## 5. URL parameters → vueData
498
+
499
+ On load: `p_router` (path, no query), `p_routers` (segment array), each query param → same-named string variable.
500
+
501
+ ```
502
+ URL: https://fp.example.vn/ts/newhocsinh?id=12345&mod=abcdef
503
+ → vueData.p_router = "/ts/newhocsinh"
504
+ → vueData.p_routers = ["ts", "newhocsinh"]
505
+ → vueData.id = "12345"
506
+ → vueData.mod = "abcdef"
507
+ ```
508
+
509
+ ```json
510
+ "data": [
511
+ {
512
+ "IF": "typeof menu != 'undefined' && menu=='0'",
513
+ "THEN": { "v_Set.menu": false }
514
+ }
515
+ ]
516
+ ```
517
+
518
+ ---
519
+
520
+ ## 6. Layout (`controls`)
521
+
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.
523
+
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`.
525
+
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.
527
+
528
+ ### Vertical centering
529
+
530
+ Verify each row's controls share a vertical center (esp. mixing input/select, button, icon, checkbox, label, differing heights).
531
+
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.
536
+
537
+ ```json
538
+ {
539
+ "prop": "row wrap align-center",
540
+ "cols": [
541
+ { "el": "v-text-field", "w": 6, "attr": { "label": "Từ khóa", "v-model": "vueData.keyword" } },
542
+ { "el": "f-button", "w": "", "col": { "class": "shrink" }, "attr": { "label": "Tìm", "icon-text": "mdi-magnify" } }
543
+ ]
544
+ }
545
+ ```
546
+
547
+ ### Exception — page is one custom component
548
+
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"`.
550
+
551
+ ```json
552
+ [
553
+ {
554
+ "prop": "fluid pa-0",
555
+ "rows": [
556
+ {
557
+ "prop": "",
558
+ "cols": [
559
+ {
560
+ "el": "uc-main-ui",
561
+ "w": "12",
562
+ "attr": {}
563
+ }
564
+ ]
565
+ }
566
+ ]
567
+ }
568
+ ]
569
+ ```
570
+
571
+ ### Grid and control object
572
+
573
+ ```json
574
+ {
575
+ "prop": "fluid grid-list-md",
576
+ "rows": [
577
+ {
578
+ "prop": "row wrap",
579
+ "cols": [
580
+ { "w": "6", "el": "v-text-field", "attr": { "label": "Tên", "v-model": "tenBien" } },
581
+ { "w": "6", "el": "v-btn", "innerHTML": "Submit" }
582
+ ]
583
+ }
584
+ ]
585
+ }
586
+ ```
587
+
588
+ ```json
589
+ {
590
+ "w": "6",
591
+ "el": "v-text-field",
592
+ "col": { "class": "py-0" },
593
+ "attr": { "label": "Tên", "v-model": "tenBien" },
594
+ "innerHTML": "..."
595
+ }
596
+ ```
597
+
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 |
605
+
606
+ ```json
607
+ {
608
+ "el": "v-btn",
609
+ "w": 6,
610
+ "col": { "class": "text-center" },
611
+ "attr": {
612
+ ":disabled": "true",
613
+ "color": "primary",
614
+ "v-on:click": "CALL(vueData.submit)"
615
+ },
616
+ "innerHTML": "Submit"
617
+ }
618
+ ```
619
+
620
+ ```json
621
+ { "el": "div", "w": "12", "innerHTML": "Xin chào {{formData.fullName}}" }
622
+ ```
623
+
624
+ ### Pixel `w` — REQUIRES `col: { "class": "shrink" }`
625
+
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.
627
+
628
+ ```json
629
+ // ❌ WRONG — 300 has no effect, column still stretches full
630
+ { "el": "v-text-field", "w": 300, "attr": { "v-model": "tuKhoa" } }
631
+
632
+ // ✅ RIGHT — shrink disables flex-grow, flex-basis 300px applies
633
+ { "el": "v-text-field", "w": 300, "col": { "class": "shrink" },
634
+ "attr": { "v-model": "tuKhoa" } }
635
+ ```
636
+
637
+ Measured (chromium, 1200px, neighbour takes rest):
638
+
639
+ | Declared | V2 | V3 |
640
+ | ------------------------------ | -------- | -------- |
641
+ | `w: 300` | 687px ❌ | 738px ❌ |
642
+ | + `col: { "shrink": true }` | 300px ✅ | 738px ❌ |
643
+ | + `col: { "class": "shrink" }` | 300px ✅ | 300px ✅ |
644
+
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.
646
+
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.
648
+
649
+ ### Toolbar row — every column `shrink` except `v-spacer`
650
+
651
+ Canonical template (filters + buttons left, group pushed right):
652
+
653
+ ```json
654
+ {
655
+ "prop": "row",
656
+ "cols": [
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": {} }
663
+ ]
664
+ }
665
+ ```
666
+
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.
668
+
669
+ Measured (1200px; 2 selects `w:150`/`w:180` + "Tìm" | `v-spacer` | 2 buttons; V3 / V2):
670
+
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 |
677
+
678
+ Row 2 is the common bug ("right-align these buttons" → spacer added, `shrink` forgotten).
679
+
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.
685
+
686
+ ### Responsive
687
+
688
+ ```json
689
+ "attr": {
690
+ ":width": "$vuetify.breakpoint.mdAndUp ? '500px' : '100%'"
691
+ }
692
+ ```
693
+
694
+ ---
695
+
696
+ ## 7. Common element patterns
697
+
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.