@fui-org/fui-cli 0.2.0 → 0.3.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.
- package/README.md +13 -1
- package/dist/fui.js +160 -43
- package/package.json +4 -2
- package/skills/fui/SKILL.md +131 -66
- package/skills/fui-skill/README.md +112 -0
- package/skills/fui-skill/SKILL.md +269 -0
- package/skills/fui-skill/assets/projectdefaultstyle.css +518 -0
- package/skills/fui-skill/design-md/airbnb/DESIGN.md +545 -0
- package/skills/fui-skill/design-md/airbnb/README.md +5 -0
- package/skills/fui-skill/design-md/airtable/DESIGN.md +554 -0
- package/skills/fui-skill/design-md/airtable/README.md +5 -0
- package/skills/fui-skill/design-md/apple/DESIGN.md +562 -0
- package/skills/fui-skill/design-md/apple/README.md +5 -0
- package/skills/fui-skill/design-md/asu/DESIGN.md +179 -0
- package/skills/fui-skill/design-md/asu/README.md +107 -0
- package/skills/fui-skill/design-md/binance/DESIGN.md +634 -0
- package/skills/fui-skill/design-md/binance/README.md +5 -0
- package/skills/fui-skill/design-md/bmw/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/bmw/README.md +5 -0
- package/skills/fui-skill/design-md/bmw-m/DESIGN.md +503 -0
- package/skills/fui-skill/design-md/bmw-m/README.md +5 -0
- package/skills/fui-skill/design-md/bugatti/DESIGN.md +454 -0
- package/skills/fui-skill/design-md/bugatti/README.md +5 -0
- package/skills/fui-skill/design-md/cal/DESIGN.md +542 -0
- package/skills/fui-skill/design-md/cal/README.md +5 -0
- package/skills/fui-skill/design-md/claude/DESIGN.md +589 -0
- package/skills/fui-skill/design-md/claude/README.md +5 -0
- package/skills/fui-skill/design-md/clay/DESIGN.md +541 -0
- package/skills/fui-skill/design-md/clay/README.md +5 -0
- package/skills/fui-skill/design-md/clickhouse/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/clickhouse/README.md +5 -0
- package/skills/fui-skill/design-md/cohere/DESIGN.md +451 -0
- package/skills/fui-skill/design-md/cohere/README.md +5 -0
- package/skills/fui-skill/design-md/coinbase/DESIGN.md +570 -0
- package/skills/fui-skill/design-md/coinbase/README.md +5 -0
- package/skills/fui-skill/design-md/composio/DESIGN.md +506 -0
- package/skills/fui-skill/design-md/composio/README.md +5 -0
- package/skills/fui-skill/design-md/cursor/DESIGN.md +537 -0
- package/skills/fui-skill/design-md/cursor/README.md +5 -0
- package/skills/fui-skill/design-md/elevenlabs/DESIGN.md +504 -0
- package/skills/fui-skill/design-md/elevenlabs/README.md +5 -0
- package/skills/fui-skill/design-md/expo/DESIGN.md +526 -0
- package/skills/fui-skill/design-md/expo/README.md +5 -0
- package/skills/fui-skill/design-md/ferrari/DESIGN.md +531 -0
- package/skills/fui-skill/design-md/ferrari/README.md +5 -0
- package/skills/fui-skill/design-md/figma/DESIGN.md +578 -0
- package/skills/fui-skill/design-md/figma/README.md +5 -0
- package/skills/fui-skill/design-md/framer/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/framer/README.md +5 -0
- package/skills/fui-skill/design-md/fui/DESIGN.md +532 -0
- package/skills/fui-skill/design-md/hashicorp/DESIGN.md +575 -0
- package/skills/fui-skill/design-md/hashicorp/README.md +5 -0
- package/skills/fui-skill/design-md/ibm/DESIGN.md +550 -0
- package/skills/fui-skill/design-md/ibm/README.md +5 -0
- package/skills/fui-skill/design-md/intercom/DESIGN.md +546 -0
- package/skills/fui-skill/design-md/intercom/README.md +5 -0
- package/skills/fui-skill/design-md/kraken/DESIGN.md +125 -0
- package/skills/fui-skill/design-md/kraken/README.md +5 -0
- package/skills/fui-skill/design-md/lamborghini/DESIGN.md +288 -0
- package/skills/fui-skill/design-md/lamborghini/README.md +5 -0
- package/skills/fui-skill/design-md/linear.app/DESIGN.md +548 -0
- package/skills/fui-skill/design-md/linear.app/README.md +5 -0
- package/skills/fui-skill/design-md/lovable/DESIGN.md +298 -0
- package/skills/fui-skill/design-md/lovable/README.md +5 -0
- package/skills/fui-skill/design-md/mastercard/DESIGN.md +365 -0
- package/skills/fui-skill/design-md/mastercard/README.md +5 -0
- package/skills/fui-skill/design-md/meta/DESIGN.md +683 -0
- package/skills/fui-skill/design-md/meta/README.md +5 -0
- package/skills/fui-skill/design-md/minimax/DESIGN.md +746 -0
- package/skills/fui-skill/design-md/minimax/README.md +5 -0
- package/skills/fui-skill/design-md/mintlify/DESIGN.md +852 -0
- package/skills/fui-skill/design-md/mintlify/README.md +5 -0
- package/skills/fui-skill/design-md/miro/DESIGN.md +825 -0
- package/skills/fui-skill/design-md/miro/README.md +5 -0
- package/skills/fui-skill/design-md/mistral.ai/DESIGN.md +773 -0
- package/skills/fui-skill/design-md/mistral.ai/README.md +5 -0
- package/skills/fui-skill/design-md/mongodb/DESIGN.md +767 -0
- package/skills/fui-skill/design-md/mongodb/README.md +5 -0
- package/skills/fui-skill/design-md/nike/DESIGN.md +575 -0
- package/skills/fui-skill/design-md/nike/README.md +5 -0
- package/skills/fui-skill/design-md/notion/DESIGN.md +821 -0
- package/skills/fui-skill/design-md/notion/README.md +5 -0
- package/skills/fui-skill/design-md/nvidia/DESIGN.md +640 -0
- package/skills/fui-skill/design-md/nvidia/README.md +5 -0
- package/skills/fui-skill/design-md/ollama/DESIGN.md +539 -0
- package/skills/fui-skill/design-md/ollama/README.md +5 -0
- package/skills/fui-skill/design-md/opencode.ai/DESIGN.md +521 -0
- package/skills/fui-skill/design-md/opencode.ai/README.md +5 -0
- package/skills/fui-skill/design-md/pinterest/DESIGN.md +597 -0
- package/skills/fui-skill/design-md/pinterest/README.md +5 -0
- package/skills/fui-skill/design-md/playstation/DESIGN.md +661 -0
- package/skills/fui-skill/design-md/playstation/README.md +5 -0
- package/skills/fui-skill/design-md/posthog/DESIGN.md +690 -0
- package/skills/fui-skill/design-md/posthog/README.md +5 -0
- package/skills/fui-skill/design-md/raycast/DESIGN.md +669 -0
- package/skills/fui-skill/design-md/raycast/README.md +5 -0
- package/skills/fui-skill/design-md/renault/DESIGN.md +589 -0
- package/skills/fui-skill/design-md/renault/README.md +5 -0
- package/skills/fui-skill/design-md/replicate/DESIGN.md +616 -0
- package/skills/fui-skill/design-md/replicate/README.md +5 -0
- package/skills/fui-skill/design-md/resend/DESIGN.md +585 -0
- package/skills/fui-skill/design-md/resend/README.md +5 -0
- package/skills/fui-skill/design-md/revolut/DESIGN.md +636 -0
- package/skills/fui-skill/design-md/revolut/README.md +5 -0
- package/skills/fui-skill/design-md/runwayml/DESIGN.md +244 -0
- package/skills/fui-skill/design-md/runwayml/README.md +5 -0
- package/skills/fui-skill/design-md/sanity/DESIGN.md +357 -0
- package/skills/fui-skill/design-md/sanity/README.md +5 -0
- package/skills/fui-skill/design-md/sentry/DESIGN.md +551 -0
- package/skills/fui-skill/design-md/sentry/README.md +5 -0
- package/skills/fui-skill/design-md/shopify/DESIGN.md +516 -0
- package/skills/fui-skill/design-md/shopify/README.md +5 -0
- package/skills/fui-skill/design-md/slack/DESIGN.md +482 -0
- package/skills/fui-skill/design-md/spacex/DESIGN.md +363 -0
- package/skills/fui-skill/design-md/spacex/README.md +5 -0
- package/skills/fui-skill/design-md/spotify/DESIGN.md +246 -0
- package/skills/fui-skill/design-md/spotify/README.md +5 -0
- package/skills/fui-skill/design-md/starbucks/DESIGN.md +580 -0
- package/skills/fui-skill/design-md/starbucks/README.md +5 -0
- package/skills/fui-skill/design-md/stripe/DESIGN.md +487 -0
- package/skills/fui-skill/design-md/stripe/README.md +5 -0
- package/skills/fui-skill/design-md/supabase/DESIGN.md +462 -0
- package/skills/fui-skill/design-md/supabase/README.md +5 -0
- package/skills/fui-skill/design-md/superhuman/DESIGN.md +448 -0
- package/skills/fui-skill/design-md/superhuman/README.md +5 -0
- package/skills/fui-skill/design-md/tesla/DESIGN.md +286 -0
- package/skills/fui-skill/design-md/tesla/README.md +5 -0
- package/skills/fui-skill/design-md/theverge/DESIGN.md +339 -0
- package/skills/fui-skill/design-md/theverge/README.md +5 -0
- package/skills/fui-skill/design-md/together.ai/DESIGN.md +633 -0
- package/skills/fui-skill/design-md/together.ai/README.md +5 -0
- package/skills/fui-skill/design-md/uber/DESIGN.md +636 -0
- package/skills/fui-skill/design-md/uber/README.md +5 -0
- package/skills/fui-skill/design-md/vercel/DESIGN.md +736 -0
- package/skills/fui-skill/design-md/vercel/README.md +5 -0
- package/skills/fui-skill/design-md/vodafone/DESIGN.md +538 -0
- package/skills/fui-skill/design-md/vodafone/README.md +5 -0
- package/skills/fui-skill/design-md/voltagent/DESIGN.md +521 -0
- package/skills/fui-skill/design-md/voltagent/README.md +5 -0
- package/skills/fui-skill/design-md/warp/DESIGN.md +526 -0
- package/skills/fui-skill/design-md/warp/README.md +5 -0
- package/skills/fui-skill/design-md/webflow/DESIGN.md +588 -0
- package/skills/fui-skill/design-md/webflow/README.md +5 -0
- package/skills/fui-skill/design-md/wired/DESIGN.md +497 -0
- package/skills/fui-skill/design-md/wired/README.md +5 -0
- package/skills/fui-skill/design-md/wise/DESIGN.md +544 -0
- package/skills/fui-skill/design-md/wise/README.md +5 -0
- package/skills/fui-skill/design-md/x.ai/DESIGN.md +465 -0
- package/skills/fui-skill/design-md/x.ai/README.md +5 -0
- package/skills/fui-skill/design-md/zapier/DESIGN.md +537 -0
- package/skills/fui-skill/design-md/zapier/README.md +5 -0
- package/skills/fui-skill/examples/component.vue +162 -0
- package/skills/fui-skill/examples/f-table-patterns.json +331 -0
- package/skills/fui-skill/examples/module-patterns.json +973 -0
- package/skills/fui-skill/examples/project-patterns.json +222 -0
- package/skills/fui-skill/metadata.json +75 -0
- package/skills/fui-skill/references/INDEX.md +144 -0
- package/skills/fui-skill/references/advanced-techniques.md +160 -0
- package/skills/fui-skill/references/coding-standards.md +112 -0
- package/skills/fui-skill/references/component-design.md +455 -0
- package/skills/fui-skill/references/component-quickref.md +77 -0
- package/skills/fui-skill/references/component-table.md +276 -0
- package/skills/fui-skill/references/components-dialog.md +192 -0
- package/skills/fui-skill/references/components-display.md +147 -0
- package/skills/fui-skill/references/components-echart.md +391 -0
- package/skills/fui-skill/references/components-input.md +359 -0
- package/skills/fui-skill/references/controls-patterns.md +847 -0
- package/skills/fui-skill/references/controls-styling-vocabulary.md +140 -0
- package/skills/fui-skill/references/db-table-design.md +77 -0
- package/skills/fui-skill/references/db-workflow.md +504 -0
- package/skills/fui-skill/references/default-function.md +415 -0
- package/skills/fui-skill/references/design-modes.md +85 -0
- package/skills/fui-skill/references/echart-templates.md +481 -0
- package/skills/fui-skill/references/fastproject.md +97 -0
- package/skills/fui-skill/references/fsheet.md +218 -0
- package/skills/fui-skill/references/fullstack-workflow.md +351 -0
- package/skills/fui-skill/references/module-data-patterns.md +126 -0
- package/skills/fui-skill/references/module-json-anatomy.md +137 -0
- package/skills/fui-skill/references/module-structure.md +260 -0
- package/skills/fui-skill/references/new-session.md +108 -0
- package/skills/fui-skill/references/pdfmake.md +60 -0
- package/skills/fui-skill/references/permission-system.md +169 -0
- package/skills/fui-skill/references/platform-architecture.md +294 -0
- package/skills/fui-skill/references/project-config.md +335 -0
- package/skills/fui-skill/references/project-provisioning.md +383 -0
- package/skills/fui-skill/references/script-map.md +296 -0
- package/skills/fui-skill/references/sql-clr-functions.md +224 -0
- package/skills/fui-skill/references/system-design.md +116 -0
- package/skills/fui-skill/references/tapi-file-api.md +191 -0
- package/skills/fui-skill/references/tapi-permission-patterns.md +158 -0
- package/skills/fui-skill/references/tapi-reference.md +549 -0
- package/skills/fui-skill/references/tools-registry.md +460 -0
- package/skills/fui-skill/references/ui-crosswindow-patterns.md +317 -0
- package/skills/fui-skill/references/ui-dialog-patterns.md +229 -0
- package/skills/fui-skill/references/ui-layout-patterns.md +176 -0
- package/skills/fui-skill/references/ui-patterns.md +315 -0
- package/skills/fui-skill/references/ui-screenshot-review.md +94 -0
- package/skills/fui-skill/references/ui-table-cell-patterns.md +316 -0
- package/skills/fui-skill/references/ui-templates.md +29 -0
- package/skills/fui-skill/references/verification.md +246 -0
- package/skills/fui-skill/references/watcher-patterns.md +196 -0
- package/skills/fui-skill/references/websocket-realtime.md +254 -0
- package/skills/fui-skill/scripts/component-3.0.js +2549 -0
- package/skills/fui-skill/scripts/component.js +3142 -0
- package/skills/fui-skill/scripts/componentTable-3.0.js +909 -0
- package/skills/fui-skill/scripts/componentTable.js +769 -0
- package/skills/fui-skill/scripts/defaultfunction-3.0.js +781 -0
- package/skills/fui-skill/scripts/defaultfunction.js +966 -0
- package/skills/fui-skill/scripts/fastproject-3.0.js +870 -0
- package/skills/fui-skill/scripts/fastproject.js +828 -0
- package/skills/fui-skill/scripts/fechart.js +890 -0
- package/skills/fui-skill/scripts/fsheet.js +1330 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Watcher Patterns (FUI Best Practice)
|
|
2
|
+
|
|
3
|
+
> File này sở hữu: **`watch`: `v_old`, deep-watch, cascading, chống race**. Cú pháp action xem [controls-patterns.md](controls-patterns.md).
|
|
4
|
+
|
|
5
|
+
Hướng này dùng cho các case:
|
|
6
|
+
- Form phụ thuộc nhiều cấp (Tỉnh/Thành -> Quận/Huyện -> Phường/Xã)
|
|
7
|
+
- Bộ lọc theo thuộc tính (status, loại, ngày, keyword, ...)
|
|
8
|
+
|
|
9
|
+
Mục tiêu:
|
|
10
|
+
- Đúng cấu trúc FUI (`module.json`: `data`, `watch`, `controls`, `set`)
|
|
11
|
+
- Tránh vòng lặp watcher
|
|
12
|
+
- Giảm gọi API thừa
|
|
13
|
+
- Dễ bảo trì khi module lớn
|
|
14
|
+
|
|
15
|
+
## 1. Nguyên tắc chuẩn
|
|
16
|
+
|
|
17
|
+
1. Chỉ watch key đầu vào nhỏ nhất
|
|
18
|
+
- Watch ID hoặc field filter (`provinceID`, `districtID`, `filter.status`), không watch cả object lớn nếu không cần.
|
|
19
|
+
|
|
20
|
+
2. Tách action theo tầng
|
|
21
|
+
- `handleProvinceChange` chỉ reset dữ liệu phụ thuộc và gọi `apiLoadDistricts`.
|
|
22
|
+
- `handleDistrictChange` chỉ reset cấp dưới và gọi `apiLoadWards`.
|
|
23
|
+
- `applyFilters` gom logic tải list cuối.
|
|
24
|
+
|
|
25
|
+
3. Dùng `deep-watch` có chọn lọc
|
|
26
|
+
- Chỉ dùng cho object filter nhỏ (ví dụ `filter`).
|
|
27
|
+
- Không deep-watch mảng lớn như `items`, `tableData`.
|
|
28
|
+
|
|
29
|
+
4. Không để watcher tự tạo loop
|
|
30
|
+
- Không watch biến output của chính action đó.
|
|
31
|
+
- Không watch `items` rồi trong callback lại ghi `items`.
|
|
32
|
+
|
|
33
|
+
5. Gắn điều kiện trước khi gọi API
|
|
34
|
+
- Dùng `IF/THEN/ELSE` để kiểm tra đầu vào hợp lệ (ví dụ chưa chọn tỉnh thì không gọi huyện).
|
|
35
|
+
|
|
36
|
+
6. Tách debounce/race-control sang `script.js`
|
|
37
|
+
- Watcher giữ vai trò điều phối.
|
|
38
|
+
- Debounce hoặc chống response cũ ghi đè response mới đặt trong helper JS.
|
|
39
|
+
|
|
40
|
+
7. Literal string chỉ ép khi truyền qua `IN`
|
|
41
|
+
- Trong action có `IN`, chuỗi không có khoảng trắng có thể bị core hiểu là expression → dùng
|
|
42
|
+
`` `value`` hoặc `"'value'"`. Quy tắc đầy đủ:
|
|
43
|
+
[controls-patterns.md §0](controls-patterns.md#0-value-resolution-rules-fundamental).
|
|
44
|
+
|
|
45
|
+
## 2. Pattern A: Cascading địa giới hành chính
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"data": [
|
|
50
|
+
{
|
|
51
|
+
"formData": {
|
|
52
|
+
"provinceID": null,
|
|
53
|
+
"districtID": null,
|
|
54
|
+
"wardID": null
|
|
55
|
+
},
|
|
56
|
+
"provinceList": [],
|
|
57
|
+
"districtList": [],
|
|
58
|
+
"wardList": []
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"apiLoadProvinces": {
|
|
62
|
+
"API": "/api/location/provinces",
|
|
63
|
+
"OUT": "provinceList"
|
|
64
|
+
},
|
|
65
|
+
"apiLoadDistricts": {
|
|
66
|
+
"API": "/api/location/districts",
|
|
67
|
+
"IN": { "ProvinceID": "formData.provinceID" },
|
|
68
|
+
"OUT": "districtList"
|
|
69
|
+
},
|
|
70
|
+
"apiLoadWards": {
|
|
71
|
+
"API": "/api/location/wards",
|
|
72
|
+
"IN": { "DistrictID": "formData.districtID" },
|
|
73
|
+
"OUT": "wardList"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"handleProvinceChange": [
|
|
78
|
+
{
|
|
79
|
+
"districtList": [],
|
|
80
|
+
"wardList": [],
|
|
81
|
+
"formData.districtID": null,
|
|
82
|
+
"formData.wardID": null
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"IF": "formData.provinceID",
|
|
86
|
+
"THEN": { "CALL": "apiLoadDistricts" }
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"handleDistrictChange": [
|
|
90
|
+
{
|
|
91
|
+
"wardList": [],
|
|
92
|
+
"formData.wardID": null
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"IF": "formData.districtID",
|
|
96
|
+
"THEN": { "CALL": "apiLoadWards" }
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"CALL": "apiLoadProvinces"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"watch": {
|
|
105
|
+
"formData.provinceID": { "CALL": "handleProvinceChange" },
|
|
106
|
+
"formData.districtID": { "CALL": "handleDistrictChange" }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Điểm chính:
|
|
112
|
+
- Watch theo key cụ thể.
|
|
113
|
+
- Reset cấp dưới trước khi gọi API cấp dưới.
|
|
114
|
+
- Có điều kiện tránh gọi API khi null.
|
|
115
|
+
|
|
116
|
+
## 3. Pattern B: Filter object + deep-watch
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"data": [
|
|
121
|
+
{
|
|
122
|
+
"filter": {
|
|
123
|
+
"keyword": "",
|
|
124
|
+
"status": null,
|
|
125
|
+
"fromDate": null,
|
|
126
|
+
"toDate": null
|
|
127
|
+
},
|
|
128
|
+
"items": []
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"apiLoadItems": {
|
|
132
|
+
"API": "/api/items/search",
|
|
133
|
+
"IN": {
|
|
134
|
+
"Keyword": "filter.keyword",
|
|
135
|
+
"Status": "filter.status",
|
|
136
|
+
"FromDate": "filter.fromDate",
|
|
137
|
+
"ToDate": "filter.toDate"
|
|
138
|
+
},
|
|
139
|
+
"OUT": "items"
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"handleFilterChanged": {
|
|
144
|
+
"CALL": "apiLoadItems"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"watch": {
|
|
149
|
+
"deep-watch": {
|
|
150
|
+
"filter": { "CALL": "handleFilterChanged" }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Khi dùng pattern này:
|
|
157
|
+
- `filter` nên nhỏ và ổn định.
|
|
158
|
+
- Nếu có text search nhập liên tục, nên debounce trong `script.js`.
|
|
159
|
+
|
|
160
|
+
## 4. Debounce khuyến nghị (script.js)
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
var filterTimer = null;
|
|
164
|
+
|
|
165
|
+
function debounceFilter(input) {
|
|
166
|
+
var callActionName = input && input.callActionName;
|
|
167
|
+
var waitMs = input && input.waitMs;
|
|
168
|
+
clearTimeout(filterTimer);
|
|
169
|
+
filterTimer = setTimeout(function () {
|
|
170
|
+
CALL(vueData[callActionName]);
|
|
171
|
+
}, waitMs || 350);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Ví dụ action trong `module.json`:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"handleFilterChanged": {
|
|
180
|
+
"FUN": "debounceFilter",
|
|
181
|
+
"IN": {
|
|
182
|
+
"callActionName": "'apiLoadItems'",
|
|
183
|
+
"waitMs": 350
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 5. Checklist QA cho watcher
|
|
190
|
+
|
|
191
|
+
1. Watch key có đủ nhỏ chưa (ID/field thay vì object lớn)?
|
|
192
|
+
2. Có reset đúng dữ liệu phụ thuộc trước khi gọi API?
|
|
193
|
+
3. Có guard `IF` trước API khi input null/rỗng?
|
|
194
|
+
4. Có nguy cơ loop watcher không?
|
|
195
|
+
5. Có cần debounce cho input text không?
|
|
196
|
+
6. Nếu action có `IN`, đã áp dụng literal string rule cho giá trị text không khoảng trắng chưa?
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# FUI Real-time Messaging — WebSocket (SignalR)
|
|
2
|
+
|
|
3
|
+
> File này sở hữu: **real-time/SignalR: `webSocketJoinGroup`, `webSocket_Send`**.
|
|
4
|
+
|
|
5
|
+
FUI cung cấp cơ chế real-time messaging dựa trên **SignalR** (không phải WebSocket API thuần của trình duyệt), host tại domain riêng `ws.fui.vn`. Toàn bộ logic client đã có sẵn trong `defaultfunction.js` (global, không cần viết lại) — chỉ cần khai báo đúng cấu trúc trong `module.json` và gọi 3 hàm sẵn có: `webSocketConnection`, `webSocketJoinGroup`, `webSocket_Send`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Import bắt buộc
|
|
10
|
+
|
|
11
|
+
Trước khi dùng, phải thêm 2 file JS qua `file_import_new` — **đúng thứ tự** (dòng dưới phụ thuộc dòng trên):
|
|
12
|
+
|
|
13
|
+
| Thứ tự | File | Ghi chú |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 1 (sort nhỏ hơn) | `https://ws.fui.vn/Scripts/jquery.signalR-2.4.1.min.js` | SignalR client core |
|
|
16
|
+
| 2 (sort lớn hơn) | `https://ws.fui.vn/signalr/hubs` | Hub proxy tự sinh từ server — vẫn là nội dung JS dù URL không có đuôi `.js` |
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
file_import_new({
|
|
20
|
+
fileType: ".js", contentType: "link",
|
|
21
|
+
fileName: "https://ws.fui.vn/Scripts/jquery.signalR-2.4.1.min.js",
|
|
22
|
+
fileSort: 100
|
|
23
|
+
})
|
|
24
|
+
file_import_new({
|
|
25
|
+
fileType: ".js", contentType: "link",
|
|
26
|
+
fileName: "https://ws.fui.vn/signalr/hubs",
|
|
27
|
+
fileSort: 110
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> jQuery đã có sẵn trong FUI runtime — không cần import thêm. Kiểm tra `file_import_list` trước để tránh add trùng (giống quy trình `f-echart` trong [components-echart.md](components-echart.md)).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Cơ chế tự động kết nối khi module load
|
|
36
|
+
|
|
37
|
+
FUI's bootstrap (`buildApp` trong `fastproject.js`) kiểm tra `vueData.webSocket` ngay sau khi `data[]` chạy xong:
|
|
38
|
+
|
|
39
|
+
- Nếu `vueData.webSocket` tồn tại → tự tạo `vueData.webSocketState = { code, text }` và gọi `webSocketConnection('https://ws.fui.vn/signalr', vueData.webSocket, vueData.webSocketState)` + `webSocketJoinGroup(vueData.webSocket)` — **không cần tự gọi 2 hàm này ở bootstrap**, chỉ cần khai báo đúng object `webSocket` trong `data[]`.
|
|
40
|
+
- `webSocketState.text` phản ánh trạng thái kết nối: `Reconnecting` / `Connected` / `Disconnected`. Có thể bind trực tiếp lên UI để debug (`innerHTML: "{{webSocketState}}"`).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 3. Khai báo group cần join — trong `data[]`
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
"data": [
|
|
48
|
+
{
|
|
49
|
+
"TenNhomDong": "`ThuNha",
|
|
50
|
+
"DGroup": "`device-abc",
|
|
51
|
+
"webSocket": {
|
|
52
|
+
"nhom1": [],
|
|
53
|
+
"nhom2": null,
|
|
54
|
+
"{{DGroup}}": {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Mỗi **key** của object `webSocket` là tên một group sẽ được join khi module load.
|
|
61
|
+
- **Kiểu giá trị quyết định cách nhận tin nhắn:**
|
|
62
|
+
- Giá trị là **array** (`[]`) → tin nhắn mới được **push** vào cuối mảng (giữ lịch sử, dùng cho chat log, danh sách sự kiện...).
|
|
63
|
+
- Giá trị là **`null` hoặc object** (`{}`) → tin nhắn mới **ghi đè** giá trị hiện tại (chỉ giữ tin mới nhất, dùng cho trạng thái/counter/notification đơn).
|
|
64
|
+
- Group name có thể là chuỗi literal (`nhom1`) hoặc **dynamic qua `{{ }}` interpolation ngay trong key** — `"{{DGroup}}"` sẽ được resolve thành giá trị hiện tại của `DGroup` (ví dụ `device-abc`), cho phép join theo group riêng của user/thiết bị.
|
|
65
|
+
- `` `ThuNha `` / `` `device-abc `` dùng backtick để ép literal string — xem [Value Resolution Rules](controls-patterns.md#0-value-resolution-rules-fundamental).
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4. Join thêm group tại runtime — `FUN: webSocketJoinGroup`
|
|
70
|
+
|
|
71
|
+
Ngoài khai báo tĩnh ở mục 3, có thể join thêm group bất cứ lúc nào (kể cả ngay trong `data[]` dưới dạng action tự chạy khi load — xem [ui-patterns.md §1](ui-patterns.md)):
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"FUN": "webSocketJoinGroup",
|
|
76
|
+
"IN": {
|
|
77
|
+
"Nhom3": null,
|
|
78
|
+
"{{TenNhomDong}}": []
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `IN` ở đây **chính là `groupObj`** (tham số đầu tiên của `webSocketJoinGroup(groupObj, timeout, callBackFunc)`) — không map theo tên tham số như hàm khác. Vì vậy `FUN` + `IN` **không truyền được `timeout`/`callBackFunc`** — nếu cần custom timeout hoặc callback sau khi join xong, dùng `EXE` raw JS: `"EXE": "webSocketJoinGroup({Nhom3:null}, 3000, function(){ ... })"`.
|
|
84
|
+
- Dùng cách này khi group cần join phát sinh **có điều kiện** hoặc **sau khi** module đã load (ví dụ: trong một action, sau khi API trả về ID phòng/chat cần join), thay vì chỉ khai báo tĩnh trong `webSocket: {}`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 5. Nhận tin nhắn
|
|
89
|
+
|
|
90
|
+
### watch (khuyến nghị — trigger logic khi có tin mới)
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
"watch": {
|
|
94
|
+
"webSocket.nhom1": { "MESS": "thong bao" },
|
|
95
|
+
"webSocket.device-abc": { "MESS": "thong bao" }
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> **Lưu ý quan trọng:** key trong `watch` **không được** dùng `{{ }}` template (watch key là literal cố định, không interpolate) — phải ghi trực tiếp giá trị đã resolve của group (ví dụ `webSocket.device-abc`, không phải `webSocket.{{DGroup}}`).
|
|
100
|
+
|
|
101
|
+
### Bind trực tiếp lên UI (hiển thị raw, không cần watch)
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"el": "v-textarea",
|
|
106
|
+
"attr": { "v-model": "webSocket.nhom2", "label": "Nội dung nhận nhom2", "rows": 5 }
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Debug toàn bộ state
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "el": "div", "attr": {}, "innerHTML": "{{webSocket}}" }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 6. Gửi tin nhắn — `webSocket_Send`
|
|
119
|
+
|
|
120
|
+
`webSocket_Send(obj)` nhận **một object** `{ group, data }` — `data` có thể là object, array, hoặc string, hàm tự chọn API SignalR phù hợp (`messageSendObject` / `messageSendArray` / `messageSend`).
|
|
121
|
+
|
|
122
|
+
### Cách 1 — `FUN` + `IN` (payload object)
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"FUN": "webSocket_Send",
|
|
127
|
+
"IN": {
|
|
128
|
+
"group": "`nhom1",
|
|
129
|
+
"data": { "text": "doi tuong 1", "text2": "doi tuong 2" }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Cách 2 — `EXE` raw JS (khi cần đọc giá trị động từ vueData)
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{ "EXE": "webSocket_Send({ group:'nhom2', data:vueData.messText })" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Cách 3 — payload dạng array
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"FUN": "webSocket_Send",
|
|
145
|
+
"IN": { "group": "`nhom3", "data": ["mang 1", "mang 2"] }
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Gọi trực tiếp trong `script.js` (ngoài action engine)
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
webSocket_Send({ group: 'Nhom3', data: 'Gui noi dung từ JS' })
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Cách 4 — Gửi qua HTTP API (không cần client tự giữ kết nối SignalR)
|
|
156
|
+
|
|
157
|
+
FUI cũng expose endpoint REST để gửi tin nhắn tới group mà không cần gọi `webSocket_Send` từ client:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"API": "https://ws.fui.vn/api/messageSend/nhom1",
|
|
162
|
+
"IN": { "text": "test 1", "text2": "test 2" }
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Group name cũng interpolate được trực tiếp trong URL:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{ "API": "https://ws.fui.vn/api/messageSend/{{DGroup}}" }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Dùng cách này khi muốn gửi tin nhắn từ một action/nơi không tiện dùng `FUN`/`EXE` (ví dụ gọi từ server, hoặc muốn tách hẳn việc gửi ra khỏi client hiện có kết nối SignalR hay không).
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 7. Ví dụ đầy đủ (tham khảo)
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"data": [
|
|
181
|
+
{
|
|
182
|
+
"TenNhomDong": "`ThuNha",
|
|
183
|
+
"DGroup": "`device-abc",
|
|
184
|
+
"webSocket": { "nhom1": [], "nhom2": null, "{{DGroup}}": {} }
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
"FUN": "webSocketJoinGroup",
|
|
188
|
+
"IN": { "Nhom3": null, "{{TenNhomDong}}": [] }
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
"watch": {
|
|
192
|
+
"webSocket.nhom1": { "MESS": "thong bao" },
|
|
193
|
+
"webSocket.device-abc": { "MESS": "thong bao" }
|
|
194
|
+
},
|
|
195
|
+
"controls": [
|
|
196
|
+
{
|
|
197
|
+
"prop": "fluid grid-list-md",
|
|
198
|
+
"rows": [
|
|
199
|
+
{
|
|
200
|
+
"prop": "",
|
|
201
|
+
"cols": [
|
|
202
|
+
{
|
|
203
|
+
"el": "v-textarea",
|
|
204
|
+
"col": { "class": "" },
|
|
205
|
+
"attr": { "v-model": "messText", "label": "noi dung", ":required": false, "rows": 5 },
|
|
206
|
+
"w": ""
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"el": "f-button",
|
|
210
|
+
"col": { "class": "shrink" },
|
|
211
|
+
"attr": {
|
|
212
|
+
"label": "send - nhom2",
|
|
213
|
+
":action": [
|
|
214
|
+
{ "FUN": "webSocket_Send", "IN": { "group": "`nhom1", "data": { "text": "doi tuong 1", "text2": "doi tuong 2" } } },
|
|
215
|
+
{ "EXE": "webSocket_Send({ group:'nhom2', data:vueData.messText})" },
|
|
216
|
+
{ "FUN": "webSocket_Send", "IN": { "group": "`nhom3", "data": ["mang 1", "mang 2"] } }
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"prop": "",
|
|
224
|
+
"cols": [
|
|
225
|
+
{ "el": "v-textarea", "col": {}, "attr": { "v-model": "webSocket.nhom2", "label": "Nội dung nhận nhom2", ":required": false, "rows": 5 } }
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
{ "prop": "", "cols": [ { "el": "div", "col": {}, "attr": {}, "innerHTML": "{{webSocket}}" } ] },
|
|
229
|
+
{
|
|
230
|
+
"prop": "",
|
|
231
|
+
"cols": [
|
|
232
|
+
{ "el": "f-button", "col": {}, "attr": { "label": "send by API - nhom1", ":action": [ { "API": "https://ws.fui.vn/api/messageSend/nhom1", "IN": { "text": "test 1", "text2": "test 2" } } ] } },
|
|
233
|
+
{ "el": "f-button", "col": {}, "attr": { ":label": "'send by API - ' + DGroup", ":action": [ { "API": "https://ws.fui.vn/api/messageSend/{{DGroup}}" } ] } }
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
{ "prop": "", "cols": [ { "el": "div", "col": {}, "attr": {}, "innerHTML": "{{webSocketState}}" } ] }
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"set": {}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 8. Checklist khi thêm real-time messaging vào module
|
|
247
|
+
|
|
248
|
+
- [ ] `file_import_list` kiểm tra đã có `jquery.signalR-2.4.1.min.js` + `signalr/hubs` chưa
|
|
249
|
+
- [ ] Thiếu → `file_import_new` cả 2, đúng thứ tự sort (signalR trước, hubs sau)
|
|
250
|
+
- [ ] Khai báo `data[].webSocket = { groupName: [] | null | {} }` — chọn kiểu giá trị theo nhu cầu (lịch sử vs. giá trị mới nhất)
|
|
251
|
+
- [ ] Group cần join runtime/điều kiện → `FUN: webSocketJoinGroup` (không dùng được `timeout`/`callBackFunc` qua `IN`, cần thì dùng `EXE`)
|
|
252
|
+
- [ ] Nhận tin: `watch` (key literal, không `{{ }}`) hoặc bind trực tiếp `v-model="webSocket.<group>"`
|
|
253
|
+
- [ ] Gửi tin: `FUN: webSocket_Send` (object/array payload) hoặc `EXE` khi cần đọc `vueData` động, hoặc `API: https://ws.fui.vn/api/messageSend/<group>` nếu muốn gửi qua HTTP thay vì client SignalR
|
|
254
|
+
- [ ] Không quên `vueData.webSocketState` nếu cần hiển thị trạng thái kết nối cho user
|