
Fui Skill
- 107 installs
- 1 repo stars
- Updated July 15, 2026
- fui-org/fui-skill
Develops FUI web modules using a metadata-driven architecture where UI and logic are defined in module.json.
About
Provides strict guidelines for building FUI web modules with a JSON-defined metadata architecture, mandatory grid layout, and action protocols. A developer uses it to create, review, or modify FUI modules in either editor or chat contexts.
- Metadata-driven module.json architecture with mandatory grid layout
- Context-aware rules for editor/workspace vs chat/agent use
Fui Skill by the numbers
- 107 all-time installs (skills.sh)
- Ranked #1,036 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fui-org/fui-skill --skill fui-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 107 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 15, 2026 |
| Repository | fui-org/fui-skill ↗ |
What it does
Develops FUI web modules using a metadata-driven architecture where UI and logic are defined in module.json.
Files
FUI Skill
Develop web modules using FUI's metadata-driven approach where UI and logic are defined in JSON.
Execution Context
Apply this skill differently depending on the environment:
- Editor / extension / workspace context: Read, create, and modify local module folders and files when the environment actually exposes them. Apply the canonical local module structure directly.
- Chat app / agent chat context: Do not assume local file access, an existing workspace, or that the module folder already exists. Work from pasted JSON, snippets, screenshots, or explicit file attachments. Still apply the canonical module structure to virtualize the expected files, organize the response, and reason about what is missing.
- Unclear context: Infer from the available tools and artifacts. If no workspace or local files are explicitly available, default to chat-safe behavior and avoid pretending to read or update files that were never provided.
Module Structure
Use this structure as the canonical folder layout for a complete FUI module in both environments. In editor or extension contexts, create or update these files directly. In chat contexts, use the same structure to virtualize the module, explain which files are involved, and provide the relevant file contents inline.
<module-name>/
|-- _info.json (Required: module metadata)
|-- module.json (Required: core data/watch/controls/set)
|-- script.js (Recommended: helper logic)
|-- dependencies.json (Recommended: external js/css)
|-- header.html (Recommended: all module/component CSS lives here)
|-- body.html (Optional)
|-- components/ (Optional custom components)
| |-- _components.json (Required when using components/)
| `-- uc-*.vue (Custom components, use `uc-` prefix)For full structure rules and checklist, see module-structure.md. Apply that reference in both workspace-aware and chat environments.
module.json Anatomy
The core file has four sections:
- `data`: Reactive state and named Actions
- `watch`: Observers triggering actions on change
- `controls`: UI layout (containers > rows > cols > elements)
- `set`: Module settings (title, menu)
Action Protocol
Define logic in data as named action objects. Execute with CALL.
| Key | Description |
|---|---|
API | Endpoint to call |
IN | Input params (use vueData. or item. refs) |
OUT | Store response (e.g., "myList") |
CALLBACK | Action after success |
CONFIRM | Show confirmation first |
MESS | Toast message |
CALL | Execute another named action |
IF/THEN/ELSE | Conditional logic |
EXE | Raw JS (use sparingly) |
Example:
"fetchUsers": {
"API": "/api/users",
"IN": { "GroupID": "vueData.selectedGroup" },
"OUT": "userList",
"CALLBACK": { "MESS": "Loaded!" }
}Literal String Rule (CRITICAL)
This rule applies when passing values inside Action Protocol IN blocks. In IN, FUI core may evaluate string values as expressions/variables when the value has no spaces.
To force a literal string value, use one of these patterns:
- Prefix with backtick: `
"id": "winUser"`or"id": "winUser"` - Wrap with single quotes inside JSON string:
"id": "'winUser'"
Recommended for fields like id, url, or any key inside IN that must stay as plain text:
{
"FUN": "openWindow",
"IN": {
"id": "`winUser",
"url": "'/fp/module?mid=123'"
}
}For Vue component props (f-*, t-*, v-*) without : in attr, values are plain strings by default. Example: "url": "/fp/module?mid=123" is already a literal string prop.
Layout System (Grid & Controls)
CRITICAL RULE: The controls array MUST follow the Mandatory Grid System structure at the top level.
1. Mandatory Grid Wrapper
All controls must be wrapped in a container > rows > cols structure:
"controls": [
{
"prop": "fluid grid-list-md",
"rows": [
{
"prop": "row",
"cols": [
// Control Objects go here
]
}
]
}
]2. Control Object Structure
Inside the cols array (or nested innerHTML), every item is a Control Object:
| Key | Type | Description |
|---|---|---|
| `el` | String | HTML tag (div, span), Vue component, or FUI component. |
| `attr` | Object | Attributes/directives (class, style, v-model, v-on:click). NO `@`. |
| `w` | Number/String | Column width. 1-12 (grid) or pixel value. REQUIRED for items in cols. |
| `col` | Object | Wrapper config. Attributes for the grid column (class, style). |
| `innerHTML` | String/Array | Content. Can be recursive array of Control Objects. |
Example:
{
"el": "v-btn",
"w": 6,
"col": { "class": "text-center" },
"attr": {
":disabled": "true",
"color": "primary",
"v-on:click": "CALL(vueData.submit)"
},
"innerHTML": "Submit"
}innerHTML Supports {{ }}
innerHTML string can use Vue template interpolation for reactive text binding.
{
"el": "div",
"w": 12,
"innerHTML": "Xin chao {{formData.fullName}}"
}Layout Rules
- Wrapper: ALWAYS start with the Grid Wrapper.
- No `children`: Use
innerHTML(array) for nesting. - Attributes: Use
attrfor element attributes. Usecolfor grid column attributes. - Events: Use
v-on:click(valid JSON), not@click.
Vuetify 2 Styling Rule
- Prefer Vuetify 2 classes first: Always try Vuetify 2 utility classes, helper classes, and standard component props before writing inline
style. - Use inline style only as fallback: Only add
stylewhen Vuetify 2 classes/props cannot express the requirement cleanly. - Common preference order:
1. Vuetify component props (color, outlined, dense, elevation, rounded, tile, justify, align) 2. Vuetify/helper classes (pa-*, ma-*, d-flex, flex-*, justify-*, align-*, text-*, primary--text, rounded-*) 3. Custom class names 4. Inline style as the last option
- No component-local style blocks: Do not write
<style>or<style scoped>inside.vuecomponents. - Header-owned styles: Put all custom CSS in
header.html. Treatheader.htmlas the canonical place for module and component styles. - No template strings in `<template>`: Never use backtick template strings inside Vue
<template></template>. Use string concatenation or computed values instead.
Reusable Component Rule
When creating components/uc-*.vue, prefer reusable building blocks over one-off screen-specific components.
- Design the component around clear props, emits, and slots before adding business-specific logic.
- Keep data fetching, routing, permissions, and page orchestration in
module.jsonor parent actions unless the component is explicitly meant to own them. - Use neutral names such as
items,value,label,loading,readonly,disabled,options, orconfiginstead of tightly coupling the component to one screen's entity names when a generic contract will work. - Emit events upward (
input,change,select,submit,remove,action) instead of mutating parent state indirectly. - Support configurable empty/loading/error states with props or slots when the component is intended for repeated use.
- Only build a highly specific component when the UI is genuinely unique to one workflow and abstraction would make it harder to maintain.
UI Templates
Copy templates from examples/ as starting points. See ui-templates.md for usage guide.
| Template | Type | Use Case |
|---|---|---|
| form-basic.json | JSON | Simple input forms |
| table-crud.json | JSON | Data tables with CRUD |
| dialog-form.json | JSON | Modal dialogs |
| component.vue | Vue | Custom component starter |
Instructions
1. Match the environment before acting:
- In editor or extension contexts with workspace access, create or update the real module root directory first (for example,
my-module/) and keep all module files inside it. - In chat or agent-chat contexts, do not claim to have created folders or edited files unless the environment actually supports it. Instead, use the same module structure virtually, present the folder tree when helpful, and provide the file contents the user should use.
- When reviewing an existing module in chat, ask for the specific files or snippets that are needed instead of assuming they are readable from disk.
2. Creating New Modules:
module.json(Required): Define UI and logic._info.json(Required): Metadata (ID, Name, Framework).dependencies.json(Optional): External libs.header.html(Optional but preferred for styling): Place all custom CSS here when the module needs styles.components/(Optional): Custom Vue components.- If the environment is chat-only, return these as separate file payloads or clearly labeled code blocks.
- When creating a custom Vue component, default to a reusable prop/event/slot API unless the user clearly needs a one-off component tied to a single screen.
- Never add
<style>or<style scoped>inside component files. Move those styles toheader.html. - Never use backtick template strings inside Vue
<template>markup.
3. Reference Docs: See references/ for component and function details:
- fastproject.md - Core action engine
- default-function.md - Utility functions
- module-structure.md - Canonical module layout and file responsibilities
- components.md - FUI
f-*component catalog with examples - component-table.md -
f-tableandf-table-viewreference - script-map.md - Quick lookup map for scripts/functions
- coding-standards.md - Class naming & Menu config
- controls-patterns.md - Action patterns & Logic
- watcher-patterns.md - Cascading and filter watcher best practices
- ui-templates.md - Template usage guide
- advanced-techniques.md - Advanced Logic & PDF patterns
- quality-assurance.md - Code Review & Edge Case Analysis
4. Module Assessment: When asked to review or assess a module, follow the Quality Assurance Protocol to identify issues, propose clean solutions, and analyze edge cases.
- In workspace-aware environments, inspect the actual module files.
- In chat contexts, review only the artifacts the user provided and say clearly when the assessment is limited by missing files.
5. Editing module.json: Use valid JSON. Reference state with vueData. prefix.
- In workspace-aware environments, edit the real
module.json. - In chat contexts, return the updated JSON content or a focused patch snippet without implying filesystem access.
6. UI Templates: Reuse the templates from examples/, then modify APIs and fields.
- In workspace-aware environments, copy or adapt the template files directly.
- In chat contexts, inline the adapted template content in the response.
Continuous Improvement
This skill is a living document. The Agent MUST actively maintain and improve it based on user feedback and project evolution.
Workflow
1. Post-Task Review: After completing a complex task, ask the user: _"Are there any lessons, patterns, or corrections from this task that should be added to the FUI skill?"_ 2. Correction & Refinement: If the user points out a mistake or a better way to do something:
- Update: Modify existing guidelines/references immediately.
- Delete: Remove obsolete or incorrect information.
- Add: Create new reference files for novel techniques.
3. Knowledge Consolidation: Periodically review references/ to merge scattered tips into cohesive guides.
Goal: Ensure fui-skill always reflects the most up-to-date, best-practice way to build FUI modules.
Common Components
f-table (Data Table with CRUD)
The f-table component supports built-in CRUD operations using update-api and update-form.
Configuration:
- `ctrl-update`: Add a header with
value: "ctrl-update"to show Action buttons. - `update-form`: Array of controls for the Add/Edit dialog.
- `update-api`: Object defining logic for
new,edit,deletekeys. Logic passed as string expressions.
Example:
{
"el": "f-table",
"attr": {
":items": "vueData.list",
":headers": [
{ "text": "Name", "value": "name" },
{ "text": "Actions", "value": "ctrl-update" }
],
":update-form": [
{ "el": "v-text-field", "attr": { "v-model": "name", "label": "Name" } }
],
":update-api": {
"new": { "list": "[...list, item]" },
"edit": { "list": "list.map(i => i.id === item.id ? item : i)" },
"delete": { "list": "list.filter(i => i.id !== item.id)" }
}
}
}.ga-1{
gap: 4px;
}
.ga-2{
gap: 8px;
}
.ga-3{
gap: 12px;
}
.ga-4{
gap: 16px;
}
.ga-5{
gap: 20px;
}
.ga-6{
gap: 24px;
}
.ga-7{
gap: 28px;
}
.ga-8{
gap: 32px;
}
.ga-9{
gap: 36px;
}
.ga-10{
gap: 40px;
}
.ga-11{
gap: 44px;
}
.ga-12{
gap: 48px;
}
/* ------------------------------------------------------------------------ */
.mw-200 {
max-width: 200px;
}
.mw-300 {
max-width: 300px;
}
.mw-400 {
max-width: 400px;
}
.mw-500 {
max-width: 500px;
}
.mw-600 {
max-width: 600px;
}
.mw-700 {
max-width: 700px;
}
.mw-800 {
max-width: 800px;
}
.mw-900 {
max-width: 900px;
}
.mw-1000 {
max-width: 1000px;
}
.mw-1200 {
max-width: 1200px;
}
.mw-1400 {
max-width: 1400px;
}
.hidden-container{
display: none;
}
/* ============================================================= */
html {
overflow-y: auto;
font-family: Roboto, sans-serif;
}
b {
font-weight: 500;
}
.wrs_editor .wrs_tickContainer {
display: none;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
/* ================================================================================================= */
/* json ace_editor */
.ace_editor.ace-jsoneditor {
font-size: 0.9em !important;
}
.jsoneditor-poweredBy {
display: none;
}
.jsoneditor {
border: none !important;
}
.jsoneditor-menu {
background-color: #1976d2 !important;
border: none !important;
}
/* jconfirm */
@media only screen and (min-width: 600px) {
.jconfirm .jconfirm-box {
min-width: 460px;
max-width: 500px;
}
}
.jconfirm .jconfirm-box {
font-family: Roboto, sans-serif !important;
border-top: 0 !important;
}
.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons button,
.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons button {
font-weight: 400 !important;
font-size: .9rem !important;
}
.jconfirm .jconfirm-box-container {
margin: auto;
max-width: none;
}
.jconfirm .jconfirm-box .jconfirm-buttons>button {
margin-left: 10px !important;
margin-right: 10px !important;
}
.jconfirm.jconfirm-white .jconfirm-box .jconfirm-title-c .jconfirm-icon-c,
.jconfirm.jconfirm-light .jconfirm-box .jconfirm-title-c .jconfirm-icon-c {
font-size: 2.9em !important;
margin: 0 0 15px 0 !important;
}
.jconfirm .jconfirm-box div.jconfirm-title-c {
display: grid;
line-height: inherit;
text-align: center;
font-size: 1.1rem !important;
}
.jconfirm .jconfirm-box div.jconfirm-content-pane.no-scroll,
.jconfirm.jconfirm-white .jconfirm-box .jconfirm-buttons,
.jconfirm.jconfirm-light .jconfirm-box .jconfirm-buttons {
text-align: center;
float: inherit;
}
/* ============================================================== */
.ck.ck-editor {
overflow-x: hidden;
font-size: initial;
}
/* =========== menu for mobile =================================================== */
@media only screen and (max-width: 959px) {
.v-application .hidden-sm-and-down {
display: none !important;
}
}
/*================== Custom style ===================================================*/
.v-text-field--filled.v-input--dense.v-text-field--outlined.v-text-field--filled>.v-input__control>.v-input__slot,
.v-text-field--filled.v-input--dense.v-text-field--outlined>.v-input__control>.v-input__slot,
.v-text-field--filled.v-input--dense.v-text-field--single-line>.v-input__control>.v-input__slot,
.v-text-field--full-width.v-input--dense.v-text-field--outlined.v-text-field--filled>.v-input__control>.v-input__slot,
.v-text-field--full-width.v-input--dense.v-text-field--outlined>.v-input__control>.v-input__slot,
.v-text-field--full-width.v-input--dense.v-text-field--single-line>.v-input__control>.v-input__slot,
.v-text-field--outlined.v-input--dense.v-text-field--outlined.v-text-field--filled>.v-input__control>.v-input__slot,
.v-text-field--outlined.v-input--dense.v-text-field--outlined>.v-input__control>.v-input__slot,
.v-text-field--outlined.v-input--dense.v-text-field--single-line>.v-input__control>.v-input__slot {
min-height: 36px;
}
.v-select.v-text-field--outlined:not(.v-text-field--single-line).v-input--dense .v-select__selections {
padding: 0px 0;
}
.v-text-field--enclosed.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__append-inner,
.v-text-field--enclosed.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__append-outer,
.v-text-field--enclosed.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__prepend-inner,
.v-text-field--enclosed.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__prepend-outer,
.v-text-field--full-width.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__append-inner,
.v-text-field--full-width.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__append-outer,
.v-text-field--full-width.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__prepend-inner,
.v-text-field--full-width.v-input--dense:not(.v-text-field--solo).v-text-field--outlined .v-input__prepend-outer {
margin-top: 6px;
}
.v-text-field--outlined.v-input--dense .v-label {
top: 8px;
}
.v-text-field input {
padding: 9px 0 7px 0;
}
.v-btn {
font-weight: 400;
}
/*=================== v-data-table ======================================================= */
.v-data-table>.v-data-table__wrapper>table>tbody>tr>td,
.v-data-table>.v-data-table__wrapper>table>tbody>tr>th,
.v-data-table>.v-data-table__wrapper>table>tfoot>tr>td,
.v-data-table>.v-data-table__wrapper>table>tfoot>tr>th,
.v-data-table>.v-data-table__wrapper>table>thead>tr>td,
.v-data-table>.v-data-table__wrapper>table>thead>tr>th {
/* padding: 0 12px; */
}
.v-select__selection--comma,
.theme--light.v-data-table thead tr th,
.theme--light.v-data-table,
.theme--light.v-input input {
/* color: #000 !important; */
font-weight: 400;
}
.theme--light.v-data-table thead tr th {
font-size: .85rem !important;
font-weight: 500;
color: #505050 !important;
height: 36px !important;
}
.v-data-table .v-text-field>.v-input__control>.v-input__slot:before {
border-width: 0 0 0 0 !important;
}
.v-data-table .v-data-table__wrapper .v-input {
font-size: inherit !important;
}
.tTable {
border-top: thin solid rgba(0, 0, 0, .08) !important;
}
.tTable table th {
white-space: nowrap !important;
background-color: #f9f9f9 !important;
border-bottom: thin solid rgba(0, 0, 0, .08) !important;
}
.tTable table td {
border-bottom: thin solid rgba(0, 0, 0, .08) !important;
}
.theme--light.v-data-table .v-data-footer {
border-top: none !important;
}
.theme--light.v-data-table .v-data-table__divider {
border-right: thin solid rgba(0, 0, 0, .08) !important;
}
.tTable table th .v-icon.v-data-table-header__icon {
display: none;
}
.tTable table th.sortable.active .v-icon.v-data-table-header__icon {
display: inline-flex;
}
.tTable tbody tr:hover {
background-color: #f5f5f5 !important;
}
.tTableNoHover tbody tr:hover {
background: transparent !important;
}
.v-data-table .v-data-table-header-mobile {
display: none !important;
}
.v-data-table .v-data-table__mobile-row {
min-height: 34px;
height: 34px !important;
}
/* ===================================================================================================
=============== scrollbar Style ============================================================================
=================================================================================================== */
.fpScrollbar {
overflow-y: auto;
}
.fpScrollbar::-webkit-scrollbar {
width: 6px;
}
/* Track */
.fpScrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
}
/* Handle */
.fpScrollbar::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
/* Handle on hover */
.fpScrollbar::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* ===================================================================================================
=============== Loader ============================================================================
=================================================================================================== */
.loader-wrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100000;
}
.loader-wrapper .loader {
display: block;
position: relative;
left: 50%;
top: 50%;
width: 111px;
height: 111px;
margin: -59px 0 0 -59px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #3498db;
-webkit-animation: fui-loader-spin 2s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
animation: fui-loader-spin 2s linear infinite;
/* Chrome, Firefox 16+, IE 10+, Opera */
z-index: 1001;
}
.loader-wrapper .loader:before {
content: "";
position: absolute;
top: 5px;
left: 5px;
right: 5px;
bottom: 5px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #e74c3c;
-webkit-animation: fui-loader-spin 3s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
animation: fui-loader-spin 3s linear infinite;
/* Chrome, Firefox 16+, IE 10+, Opera */
}
.loader-wrapper .loader:after {
content: "";
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
border-radius: 50%;
border: 3px solid transparent;
border-top-color: #f9c922;
-webkit-animation: fui-loader-spin 1.5s linear infinite;
/* Chrome, Opera 15+, Safari 5+ */
animation: fui-loader-spin 1.5s linear infinite;
/* Chrome, Firefox 16+, IE 10+, Opera */
}
@-webkit-keyframes fui-loader-spin {
0% {
-webkit-transform: rotate(0deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg);
/* IE 9 */
transform: rotate(0deg);
/* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg);
/* IE 9 */
transform: rotate(360deg);
/* Firefox 16+, IE 10+, Opera */
}
}
@keyframes fui-loader-spin {
0% {
-webkit-transform: rotate(0deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(0deg);
/* IE 9 */
transform: rotate(0deg);
/* Firefox 16+, IE 10+, Opera */
}
100% {
-webkit-transform: rotate(360deg);
/* Chrome, Opera 15+, Safari 3.1+ */
-ms-transform: rotate(360deg);
/* IE 9 */
transform: rotate(360deg);
/* Firefox 16+, IE 10+, Opera */
}
}
/* ===================================================================================================
=============== Loader ============================================================================
=================================================================================================== */<template>
<div class="uc-component-name">
<v-card outlined>
<v-card-title v-if="title" class="subtitle-1 font-weight-bold">
<slot name="title">
{{ title }}
</slot>
</v-card-title>
<v-card-text>
<div v-if="loading">
<slot name="loading">
Loading...
</slot>
</div>
<div v-else-if="!hasItems">
<slot name="empty">
No data
</slot>
</div>
<div v-else>
<slot :items="localItems" :selected-item="localValue">
<v-list dense>
<v-list-item
v-for="(item, index) in localItems"
:key="item.id || item.value || index"
@click="handleSelect(item)"
>
<v-list-item-content>
<v-list-item-title>{{ getItemLabel(item) }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</slot>
<v-btn
v-if="showAction"
small
color="primary"
:disabled="disabled"
@click="handleAction"
>
{{ actionLabel }}
</v-btn>
</div>
</v-card-text>
</v-card>
</div>
</template>
<script>
/**
* Component Name: uc-component-name
* Description: Reusable starter component. Prefer generic props, emits, and slots.
* Usage: <uc-component-name :items="rows" :value="selectedRow" @select="handleSelect"></uc-component-name>
*/
export default {
props: {
title: {
type: String,
default: ''
},
value: {
type: [String, Number, Object, Array],
default: null
},
items: {
type: Array,
default: function() {
return [];
}
},
itemText: {
type: String,
default: 'label'
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
showAction: {
type: Boolean,
default: true
},
actionLabel: {
type: String,
default: 'Action'
},
options: {
type: Object,
default: function() {
return {};
}
}
},
data() {
return {
localValue: null,
localItems: []
}
},
computed: {
hasItems() {
return this.localItems.length > 0;
},
resolvedItemText() {
return this.options.itemText || this.itemText;
},
resolvedItemValue() {
return this.options.itemValue || 'value';
}
},
watch: {
value: {
handler(val) {
this.localValue = val;
},
immediate: true
},
items: {
handler(val) {
this.localItems = Array.isArray(val) ? val : [];
},
immediate: true
}
},
methods: {
getItemLabel(item) {
if (!item || typeof item !== 'object') {
return item;
}
return item[this.resolvedItemText] || item.label || item.name || '';
},
getItemValue(item) {
if (!item || typeof item !== 'object') {
return item;
}
return item[this.resolvedItemValue] || item.value || item.id || item;
},
handleSelect(item) {
var nextValue = this.getItemValue(item);
this.localValue = nextValue;
this.$emit('input', nextValue);
this.$emit('select', item);
this.$emit('change', nextValue);
},
handleAction() {
this.$emit('action', {
value: this.localValue,
items: this.localItems
});
}
}
}
</script>
{
"_comment": "Dialog Template - Modal dialog with form controls and action buttons",
"data": [
{
"dialogVisible": false,
"dialogData": {
"field1": "",
"field2": null
}
},
{
"openDialog": {
"dialogVisible": true,
"dialogData": "item"
},
"closeDialog": {
"dialogVisible": false
}
}
],
"controls": [
{
"prop": "fluid",
"rows": [
{
"prop": "",
"cols": [
{
"el": "v-btn",
"attr": {
"color": "primary",
"v-on:click": "CALL(vueData.openDialog, {item: {field1: '', field2: null}})"
},
"innerHTML": "Mở Dialog",
"w": "12"
},
{
"el": "f-dialog",
"attr": {
"v-model": "dialogVisible",
":data.sync": "dialogData",
"title": "Tiêu đề Dialog",
":width": "$vuetify.breakpoint.mdAndUp ? 500 : '90%'",
":controls": [
{
"el": "v-text-field",
"attr": {
"v-model": "field1",
"label": "Trường 1",
"outlined": true,
"dense": true
}
},
{
"el": "v-select",
"attr": {
"v-model": "field2",
"label": "Trường 2",
":items": [
{
"text": "Option A",
"value": "A"
},
{
"text": "Option B",
"value": "B"
}
],
"outlined": true,
"dense": true
}
}
],
":button": [
{
"label": "Hủy",
"color": "grey",
"action": {
"dialogVisible": false
}
},
{
"label": "Lưu",
"color": "primary",
"getoutdata": true,
"action": {
"API": "/api/save",
"IN": {
"Field1": "field1",
"Field2": "field2"
},
"CALLBACK": [
{
"dialogVisible": false
},
{
"MESS": "Đã lưu!"
}
]
}
}
]
},
"w": "12"
}
]
}
]
}
]
}{
"_comment": "Basic Form Template - Use as starting point for data input forms",
"data": [
{
"formData": {
"field1": "",
"field2": null,
"field3": false
}
},
{
"submitForm": {
"CONFIRM": "Xác nhận lưu dữ liệu?",
"API": "/api/endpoint",
"IN": {
"Field1": "formData.field1",
"Field2": "formData.field2",
"Field3": "formData.field3"
},
"CALLBACK": {
"MESS": "Lưu thành công!"
}
}
}
],
"controls": [
{
"prop": "fluid grid-list-md",
"rows": [
{
"prop": "row",
"cols": [
{
"el": "v-text-field",
"attr": {
"v-model": "formData.field1",
"label": "Trường văn bản",
"outlined": true,
"dense": true,
":required": true
},
"w": "6"
},
{
"el": "v-select",
"attr": {
"v-model": "formData.field2",
"label": "Chọn giá trị",
":items": [
{
"text": "Tùy chọn 1",
"value": 1
},
{
"text": "Tùy chọn 2",
"value": 2
}
],
"outlined": true,
"dense": true
},
"w": "6"
},
{
"el": "v-checkbox",
"attr": {
"v-model": "formData.field3",
"label": "Đồng ý điều khoản"
},
"w": "12"
},
{
"el": "v-btn",
"attr": {
"color": "primary",
"v-on:click": "CALL(vueData.submitForm)"
},
"innerHTML": "Lưu",
"w": "12"
}
]
}
]
}
]
}{
"_comment": "Data Table Template - CRUD table with search, add, edit, delete",
"data": [
{
"items": [],
"searchText": ""
},
{
"apiLoadData": {
"API": "/api/items/list",
"OUT": "items"
}
},
{
"CALL": "apiLoadData"
}
],
"controls": [
{
"prop": "fluid",
"rows": [
{
"prop": "row",
"cols": [
{
"el": "f-table",
"attr": {
"label": "Danh sách dữ liệu",
":items": "items",
"item-key": "ID",
"show-search": true,
"excel": true,
":headers": [
{
"text": "Tên",
"value": "Name",
"divider": true
},
{
"text": "Mô tả",
"value": "Description",
"divider": true
},
{
"text": "Trạng thái",
"value": "Status",
"align": "center",
"width": "100px",
"el": "t-boolean"
},
{
"text": "Cập nhật",
"value": "ctrl-update",
"align": "center",
"width": "100px"
}
],
":update-form": [
{
"el": "v-text-field",
"attr": {
"v-model": "Name",
"label": "Tên",
":required": true
}
},
{
"el": "v-textarea",
"attr": {
"v-model": "Description",
"label": "Mô tả",
":rows": 2
}
}
],
":update-api": {
"default-item": {
"Name": "",
"Description": ""
},
"new": {
"API": "/api/items/create",
"IN": {
"Name": "item.Name",
"Description": "item.Description"
},
"CALLBACK": {
"CALL": "apiLoadData"
}
},
"edit": {
"API": "/api/items/update",
"IN": {
"ID": "item.ID",
"Name": "item.Name",
"Description": "item.Description"
},
"CALLBACK": {
"CALL": "apiLoadData"
}
},
"delete": {
"CONFIRM": "Xác nhận xóa?",
"API": "/api/items/delete",
"IN": {
"ID": "item.ID"
},
"CALLBACK": {
"CALL": "apiLoadData"
}
}
}
},
"w": "12"
}
]
}
]
}
]
}FUI Skill
Agent Skill hỗ trợ AI phát triển web module theo kiến trúc metadata-driven của FUI — nơi giao diện và logic được định nghĩa hoàn toàn bằng JSON.
✨ Tổng quan
FUI Skill cung cấp cho AI agent các hướng dẫn và công cụ để xây dựng FUI web module, bao gồm:
- 📐 Kiến trúc metadata-driven — Giao diện và logic định nghĩa trong
module.json - 🧱 Hệ thống grid bắt buộc — Cấu trúc container > rows > cols nhất quán
- ⚡ Action Protocol — Gọi API, điều kiện, callback theo cách khai báo
- 🧩 Template có sẵn — Form, CRUD table, dialog, Vue component
- 🔍 Đảm bảo chất lượng — Review code và phân tích edge-case tích hợp
- 🛠️ Công cụ module — Lưu và publish module/component qua script
📦 Cài đặt
npx skills add fui-labs/fui-skill📁 Cấu trúc thư mục
fui-skill/
├── SKILL.md # Hướng dẫn chính của skill
├── examples/ # Template mẫu
│ ├── form-basic.json # Form nhập liệu cơ bản
│ ├── table-crud.json # Bảng dữ liệu với CRUD
│ ├── dialog-form.json # Dialog dạng modal
│ └── component.vue # Component Vue mẫu
├── references/ # Tài liệu tham khảo chi tiết
│ ├── fastproject.md # Core action engine
│ ├── default-function.md # Các hàm tiện ích
│ ├── coding-standards.md # Quy tắc đặt tên & cấu hình menu
│ ├── controls-patterns.md # Mẫu action & logic
│ ├── ui-templates.md # Hướng dẫn sử dụng template
│ ├── components.md # Tham khảo component
│ ├── component-table.md # Hướng dẫn f-table
│ ├── advanced-techniques.md # Logic nâng cao & PDF
│ └── quality-assurance.md # Quy trình review code
├── scripts/ # Mã nguồn runtime
│ ├── component.js
│ ├── componentTable.js
│ ├── defaultfunction.js
│ └── fastproject.js
└── assets/
└── projectdefaultstyle.css🚀 Bắt đầu nhanh
Sau khi cài đặt, agent sẽ tự động sử dụng skill này khi bạn yêu cầu tạo, chỉnh sửa hoặc review FUI module.
Tạo module mới
"Tạo module `user-management` có bảng dữ liệu và dialog thêm/sửa."
Agent sẽ: 1. Tạo thư mục module với module.json, _info.json, script.js 2. Áp dụng hệ thống grid layout bắt buộc 3. Sử dụng Action Protocol để tích hợp API 4. Tuân theo coding standards và best practices
Các khái niệm chính
| Khái niệm | Mô tả |
|---|---|
| module.json | File chính định nghĩa data, watch, controls, và set |
| Action Protocol | Khai báo action bằng API, IN, OUT, CALL, IF/THEN/ELSE |
| Grid System | Cấu trúc bắt buộc container > rows > cols cho mọi layout |
| Control Object | Phần tử UI được định nghĩa bởi el, attr, w, col, innerHTML |
Ví dụ — Lấy dữ liệu bằng Action
{
"fetchUsers": {
"API": "/api/users",
"IN": { "GroupID": "vueData.selectedGroup" },
"OUT": "userList",
"CALLBACK": { "MESS": "Tải thành công!" }
}
}📚 Tài liệu tham khảo
| Tài liệu | Mô tả |
|---|---|
| fastproject.md | Tài liệu core action engine |
| default-function.md | Các hàm tiện ích có sẵn |
| coding-standards.md | Quy tắc đặt tên & cấu hình menu |
| controls-patterns.md | Mẫu action & logic điều kiện |
| ui-templates.md | Hướng dẫn sử dụng template |
| advanced-techniques.md | Logic nâng cao & tạo PDF |
| quality-assurance.md | Review code & phân tích edge-case |
🤝 Đóng góp
Skill này là tài liệu sống, liên tục được cập nhật. Sau khi hoàn thành các task phức tạp, agent sẽ hỏi:
"Có bài học, pattern hoặc chỉnh sửa nào từ task này cần bổ sung vào FUI skill không?"
Phản hồi sẽ được tích hợp bằng cách cập nhật hướng dẫn, thêm tài liệu mới, hoặc loại bỏ thông tin lỗi thời — đảm bảo skill luôn phản ánh best practices mới nhất.
📄 Giấy phép
MIT
Advanced FUI Techniques
This document captures advanced patterns and techniques for FUI development, extracted from real-world implementations.
1. Advanced Logic & Scripting
FUI's JSON-based logic has limitations. Use script.js for complex operations.
Computed Properties Workaround
FUI module.json does not natively support computed properties. Solution: Define calculation functions in script.js and call them directly in module.json.
script.js:
function getTongTien() {
return vueData.items.reduce((sum, item) => sum + (item.amount || 0), 0);
}module.json:
{
"innerHTML": "Total: {{getTongTien().toLocaleString()}} VNĐ"
}Complex Validation
Avoid writing long logic strings in JSON. Move validation logic to script.js.
script.js:
function validateStep1() {
const d = vueData;
if (!d.name || !d.email) {
alert("Missing required fields!");
return false;
}
return true;
}
function nextStep() {
if (vueData.step === 1 && !validateStep1()) return;
vueData.step++;
}module.json:
"v-on:click": "nextStep()"Action Binding
You can bind direct JS functions to events instead of using the CALL action protocol if needed for simple UI logic.
"v-on:click": "prevBuoc()"---
2. PDF Generation (pdfmake)
Techniques for generating complex PDFs using pdfmake within FUI.
Dynamic Tables
Map array data to table rows dynamically.
/* script.js */
body: [
[{ text: 'STT', bold: true }, { text: 'Name', bold: true }],
...data.items.map(item => ([
item.index,
item.name
]))
]SVG Checkboxes
Unicode characters (☑/☐) may fail to render in some PDF fonts. Use SVG paths instead.
const checkedSvg = '<svg ...>...</svg>';
const uncheckedSvg = '<svg ...>...</svg>';
{
svg: item.isChecked ? checkedSvg : uncheckedSvg,
width: 14
}Complex Layouts
Use columns for layouts that standard tables can't handle easily (like checkboxes side-by-side with text).
{
columns: [
{ svg: checkedSvg, width: 14 },
{ text: ' Label text', width: '*' }
]
}Signature Section Table
Use a nested table structure for signature blocks to ensure alignment.
table: {
widths: ['16%', '16%', ...], // 6 columns
body: [
[{ text: 'Title', colSpan: 3 }, {}, {} ...], // Header spanning columns
['Sign 1', 'Sign 2', ...] // Individual signature slots
]
}FUI Coding Standards
1. Naming Conventions
Components (components/)
- Prefix: Use
uc-(User Component) for custom module components to distinguish from standard FUI components (f-,t-). - Format: Kebab-case (e.g.,
uc-trangthai-sukien.vue,uc-user-profile.vue). - Props: Use camelCase in script, kebab-case in templates (e.g.,
userProfile->:user-profile). - Prefer reusable APIs: Default to generic props, emits, and slots so the component can be reused across modules or screens.
- Avoid overfitting names: Prefer neutral contracts like
items,value,label,loading,readonly,disabled,options,configunless the business domain truly requires a specific prop name. - Push orchestration upward: Keep API calls, route changes, and page-specific coordination in
module.jsonor the parent when possible. Let the component focus on presentation and local interaction. - Emit upward: Prefer
$emit(...)forinput,change,select,submit,remove, or explicit action events rather than mutating parent-owned state.
CSS Classes
- No `<style>` Tags in `.vue`: Do not use
<style>or<style scoped>in.vuecomponent files. - Style Placement: All custom styles must go in
header.htmlusing a<style>block in the head area. Do not place module/component CSS instyles/index.css. - Vuetify Utilities: Prioritize Vuetify helper classes (e.g.,
ma-2,pa-0,d-flex,primary--text). - Custom Classes: Use meaningful prefixed names (e.g.,
ep-hero,ep-field-grid). Avoid generic names like.boxor.red. - State Classes: Use descriptive names for state (e.g.,
.is-active,.has-error).
Action Keys (module.json)
- API Actions: Predix with
api(e.g.,apiGetDSSuKien,apiUpdateUser). - Event Handlers: Prefix with
handle(e.g.,handleOpenReport,handleSubmit). - Dialog Actions: Prefix with verb (e.g.,
openUploadDialog,closeSettings).
2. Menu Configuration (set.menu)
Define the application menu in the set object of module.json.
"menu": [
{
"name": "Main Group",
"icon": "mdi-home", // Material Design Icons
"url": "/dashboard", // Route
"right": { // Permission check
"SystemRight": [1, 2] // Array of allowed Right IDs
}
},
{
"name": "Management",
"icon": "mdi-cog",
"submenu": [ // Nested menu items
{
"name": "Users",
"url": "/users"
},
{
"name": "Settings",
"url": "/settings"
}
]
}
]3. Project Structure
- Canonical module structure: Follow module-structure.md as the default module layout in both workspace and chat contexts. In workspace-aware environments, apply it to real local files. In chat contexts, use it to virtualize the same module structure in the response.
- Required core files: Keep
_info.jsonandmodule.jsonat module root. - `components/`: Only place
.vuefiles here. Do not sub-folder unless strictly necessary (FUI auto-scans this root). - `components/_components.json`: Maintain component registry when using
uc-*components. - `header.html`: Use this as the canonical location for module and component CSS.
- `module.json`: Keep this file clean. Move large static lists to the database or separate JSON files if supported.
4. Best Practices
- Data Binding: Avoid complex logic in JSON attributes. Use computed properties in components or simpler
vueDatastructures. - Event Handling: Use
CALL(vueData.actionName)for all complex interactions. Avoid inline JS likevueData.count++for anything beyond simple toggles. - Mobile Responsiveness: Always configure
configForm.xsandconfigForm.mdfor responsive form widths.
5. Component Registration (_components.json)
Components are registered using an upsert pattern:
- New component (before publish): Only
comNameis required. Do NOT manually assigncomID.
[
{ "comName": "hr-employee-profile" }
]- After publish & sync: The server auto-assigns
comID. The file gets updated on sync:
[
{ "comID": 7813, "comName": "hr-employee-profile" }
]Rule: Never manually create or modify comID. It is server-generated.6. Vue Template Syntax Constraints
Inside <template> of .vue files:
- No template strings (backticks): Use string concatenation instead.
- Never use backticks inside `<template>`: This applies to bindings, labels, class expressions, and inline text assembly inside Vue templates.
<!-- ❌ WRONG -->
:label="`Total (${items.length})`"
<!-- ✅ CORRECT -->
:label="'Total (' + items.length + ')'"- No arrow functions: Use
function()syntax for broader compatibility.
// ❌ WRONG
props: { items: { default: () => [] } }
// ✅ CORRECT
props: { items: { default: function() { return [] } } }7. Complex Module Architecture
When a module's module.json controls exceed ~200 lines:
- Extract to Vue component: Move the UI into a
.vuecomponent. Keepmodule.jsonlean (data/API only + a single component call incontrols). - Props binding: Pass all data from
module.jsonvia props. Use kebab-case for prop names in templates (:nhan-vien="nhanVien"). - Design for reuse first: Before naming props or methods, check whether the component can be expressed as a generic list, card, dialog body, filter panel, summary block, or form section reused by other modules.
- Configurable states: Expose loading, empty, disabled, and readonly behavior through props or slots instead of hardcoding one workflow.
- Sticky headers: When combining multiple sticky elements (e.g., hero + tabs), wrap them in one parent div with
position: stickyinstead of making each element sticky individually. - Tab navigation vs Accordion: For 5+ sections of data, prefer horizontal
v-tabsoverv-expansion-panels. Tabs show one section at a time, reduce cognitive overload, and support swipe gestures on mobile. - Swipe gestures: Attach
touchstart/touchendlisteners on the outermost wrapper (not on content area) so swipe works regardless of content height.
FUI Table Components (Source-Based)
This file is derived from scripts/componentTable.js. Focus: f-table, f-table-view, and t-* renderers.
Prefix Reminder
f-table,f-table-view: FUI components.t-*: FUI cell renderers for table columns.v-data-table: Vuetify internal implementation, but CRUD and table action flow comes from FUI wrappers.
f-table
Props (exact from source)
value, headers, updateForm, updateFormAttr, updateApi, items, label, excel, showSearch, sumFormat, disabledHover, returnObject, itemKey, itemsPerPage, disabledUpdate, hideDefaultFooter, template, headercomponent
Real Behavior
1. If a header item has el, table creates custom slot renderer automatically. 2. If updateApi.new or updateApi['new-action'] exists, add button appears. 3. CRUD action slots rely on header value: "ctrl-update". 4. Editing dialog is f-dialog with updateForm. 5. Selection result emit:
- if
returnObject=false: emit array ofitemKey - if
returnObject=true: emit selected row objects
6. Emits row events from slots: click:row, mouseover:row, mouseleave:row.
updateApi Keys Used By Source
neweditdeletedefault-itemnew-actionedit-actiondata-out
Minimal CRUD Example
{
"el": "f-table",
"w": "12",
"attr": {
"label": "Danh sach nhan vien",
":items": "items",
"item-key": "ID",
":return-object": false,
"show-search": true,
"excel": true,
":headers": [
{ "text": "Ho ten", "value": "FullName" },
{ "text": "Email", "value": "Email" },
{ "text": "Trang thai", "value": "Active", "el": "t-boolean", "align": "center", "width": "90px" },
{ "text": "Tac vu", "value": "ctrl-update", "align": "center", "width": "120px" }
],
":update-form": [
{ "el": "v-text-field", "attr": { "v-model": "FullName", "label": "Ho ten", ":required": true } },
{ "el": "v-text-field", "attr": { "v-model": "Email", "label": "Email", ":required": true } }
],
":update-api": {
"default-item": { "FullName": "", "Email": "", "Active": true },
"new": {
"API": "/api/staff/create",
"IN": { "FullName": "item.FullName", "Email": "item.Email", "Active": "item.Active" },
"CALLBACK": { "CALL": "apiLoadItems" }
},
"edit": {
"API": "/api/staff/update",
"IN": { "ID": "item.ID", "FullName": "item.FullName", "Email": "item.Email", "Active": "item.Active" },
"CALLBACK": { "CALL": "apiLoadItems" }
},
"delete": {
"CONFIRM": "Xac nhan xoa?",
"API": "/api/staff/delete",
"IN": { "ID": "item.ID" },
"CALLBACK": { "CALL": "apiLoadItems" }
}
}
}
}f-table-view
Props (exact from source)
value, items, label, itemKey, excel, itemsPerPage, disabledUpdate, allwayVisible, showSearch, disabledHover, hideDefaultFooter
Real Behavior
1. Auto-build headers from data keys (buildHeader). 2. If items exists and disabledUpdate=false, auto-add delete column ctrl-delete. 3. If table does not use show-select, component emits full dataItems to v-model. 4. Supports search and excel export.
Minimal Example
{
"el": "f-table-view",
"w": "12",
"attr": {
"label": "Danh sach xem nhanh",
":items": "items",
"item-key": "ID",
"show-search": true,
"excel": true,
":items-per-page": 20,
":allway-visible": true
}
}t-* Cell Renderers
Use in headers by setting el.
Example header:
{ "text": "Ngay tao", "value": "CreatedAt", "el": "t-time", "attr": { "format": "DD/MM/YYYY HH:mm" } }Supported t-* components
t-htmlt-labelt-numt-timet-booleant-checkt-textt-selectt-comboboxt-menut-linkt-button
Notes
1. Editable renderers (t-check, t-text, t-select, t-combobox, t-menu, t-button) use tableActionEvent. 2. For dialog opening in row actions, use target: "dialog" plus wid, url, title, onclose. 3. Apply literal-string rule only in Action IN mapping. Static component props without : are plain strings by default.
FUI Components (Source-Based)
This file is derived from scripts/component.js and focuses on FUI components with prefix f-. If docs and source differ, follow source.
Prefix and Binding Rules
f-*: FUI components (defined in FUI runtime).v-*: Vuetify components (Vuetify props/events).uc-*: Custom module components fromcomponents/.- In
module.json, pass component props viaattr. - Prefer kebab-case in JSON for custom props (
api-upload,item-value,time-add). - For dynamic values use
:prop. - Use
v-on:*in JSON (not@*). - For component props without
:(for exampleurl,api-upload,imageapi), value is a plain string prop by default. - Backtick/quote literal forcing is for Action
INmapping, not normal static component props.
Minimal Control Pattern
{
"el": "f-date",
"w": "6",
"attr": {
"v-model": "formData.fromDate",
"label": "From date"
}
}Component Catalog
f-label
- Props (source):
items,text,color,iconText - Behavior: renders
v-chipfromtextand/oritems - Example:
{
"el": "f-label",
"w": "12",
"attr": {
"text": "Trang thai",
"icon-text": "mdi-information"
}
}f-box
- Props (source):
rowFormat,items,label,edit,saveclick - Emit:
inputwhen internal edited data changes - Behavior: display/edit key-value rows, auto-build row format from data if missing
- Example:
{
"el": "f-box",
"w": "12",
"attr": {
"label": "Thong tin",
":items": "detailData",
":edit": "isEditMode"
}
}f-header
- Props (source):
label - Example:
{
"el": "f-header",
"w": "12",
"attr": { "label": "Bao cao tong hop" }
}f-title
- Props (source):
label - Example:
{
"el": "f-title",
"w": "12",
"attr": { "label": "Danh sach ho so" }
}f-radiobox
- Props (source):
label,items,itemValue,itemText - Emit:
inputon change - Example:
{
"el": "f-radiobox",
"w": "6",
"attr": {
"label": "Loai",
"v-model": "formData.loai",
":items": "loaiOptions",
"item-value": "value",
"item-text": "text"
}
}f-menu
- Props (source):
items,label,iconText,menuAttr - Behavior: each item can run
item.actionthroughrunAction - Example:
{
"el": "f-menu",
"w": "3",
"attr": {
"label": "Tac vu",
":items": "menuActions",
"icon-text": "mdi-dots-vertical"
}
}f-search
- Props (source):
api,apiData,items - Emit:
inputwhen selection changes - Behavior: debounced API search (500ms), ignores keyword length < 2
- Example:
{
"el": "f-search",
"w": "6",
"attr": {
"v-model": "filter.userID",
"api": "/api/user/search",
":api-data": { "Keyword": "TEXT", "GroupID": "vueData.groupID" },
"item-text": "UserName",
"item-value": "UserID"
}
}f-button
- Props (source):
label,checkvalid,ctrlhotkey,iconText,action,includeData - Behavior:
- debounced click (500ms, leading)
- validates form when
checkvalid=true - if
$attrs.target == 'dialog', opens window by attrs (wid,title,url,onclose) - Example:
{
"el": "f-button",
"w": "3",
"attr": {
"label": "Luu",
"color": "primary",
":action": { "CALL": "handleSubmit" }
}
}f-slider
- Props (source):
items,itemAttr - Behavior: wraps
v-carouselandv-carousel-item - Example:
{
"el": "f-slider",
"w": "12",
"attr": {
":items": "slideItems",
":item-attr": { "contain": true }
}
}f-file-upload
- Props (source):
label,url,autoclose,iconText,filters,resize,onclose - Emit:
inputon upload complete - Behavior:
- uses Plupload
- auto-upload on file selection
- runs
oncloseaction when dialog closes - Example:
{
"el": "f-file-upload",
"w": "4",
"attr": {
"label": "Tai tep",
"url": "/api/upload/file",
":filters": {
"max_file_size": "20mb",
"mime_types": [{ "title": "PDF", "extensions": "pdf" }]
},
":onclose": { "CALL": "handleUploadClosed" }
}
}f-qrcode
- Props (source):
value,logo,color,size - Behavior: renders QR image from value
- Example:
{
"el": "f-qrcode",
"w": "4",
"attr": {
"v-model": "formData.qrText",
":size": 200,
"color": "#1e90ff"
}
}f-qrcode-reader
- Props (source):
value,label,width - Emit:
input(close dialog),update(decoded content) - Example:
{
"el": "f-qrcode-reader",
"w": "12",
"attr": {
"v-model": "scanDialog",
"label": "Doc ma QR",
"v-on:update": "CALL(vueData.handleScanResult, { item: $event })"
}
}f-image-update
- Props (source):
value,label,title,apiUpload,iconText,imageType,size,quality,dialogWidth,dialogHeight,imageBoxAttr,imageAttr,buttonAttr,croperAttr - Emit:
updateafter successful API upload ({ returnData, imageData }) - Example:
{
"el": "f-image-update",
"w": "4",
"attr": {
"v-model": "formData.avatar",
"label": "Anh dai dien",
"api-upload": "/api/upload/avatar",
":dialog-width": 600,
":dialog-height": 420
}
}f-date
- Props (source):
value,label,dateAdd,required - Emit:
inputwith normalized date (ornull) - Behavior:
- supports typed mask and picker
- accepts multiple input formats in initialization
dateAddauto-sets date relative to today- Example:
{
"el": "f-date",
"w": "6",
"attr": {
"v-model": "filter.fromDate",
"label": "Tu ngay",
":required": false,
":date-add": -7
}
}f-time
- Props (source):
value,label,timeAdd,required - Emit:
inputwithHH:mm(ornull) - Behavior: typed mask + time picker
- Example:
{
"el": "f-time",
"w": "6",
"attr": {
"v-model": "formData.startTime",
"label": "Gio bat dau",
":required": false,
":time-add": 30
}
}f-time-counter
- Props (source):
value,type,labelFormat,format - Emit:
input(countdown seconds) when counting from secondstime-endwhen timer reaches 0- Example:
{
"el": "f-time-counter",
"w": "4",
"attr": {
":value": 300,
":type": 1,
":format": { "label": "Con lai: ", "second": " giay" }
}
}f-chart
- Props (source):
data,type,options,reverseData - Behavior: wraps Chart.js and rebuilds chart when
datachanges - Example:
{
"el": "f-chart",
"w": "12",
"attr": {
":data": "chartData",
"type": "bar",
":options": "chartOptions",
":reverse-data": false
}
}f-chart-data-viewer
- Props (source):
label,data - Behavior: computes percentage display from dataset
- Example:
{
"el": "f-chart-data-viewer",
"w": "12",
"attr": {
"label": "Chi tiet bieu do",
":data": "chartViewerData"
}
}f-window
- Props (source):
id - Behavior: internal iframe dialog host, usually opened by
openWindow(...) - Recommended action example:
{
"FUN": "openWindow",
"IN": {
"id": "`winUser",
"url": "'/fp/module?mid=123'",
"title": "Chi tiet user",
"width": 1100
}
}f-editor
- Props (source):
value,imageapi,height,toolbar - Emit:
inputwith HTML content - Behavior: wraps CKEditor with upload auth header
- Example:
{
"el": "f-editor",
"w": "12",
"attr": {
"v-model": "formData.contentHtml",
"imageapi": "/api/editor/upload",
":height": 360
}
}f-editor-dialog
- Props (source):
value,imageapi,label,width,toolbar - Runtime expectation:
valueis used as object (open,text,object,ok), not plain string - Behavior: modal editor and write-back on OK
- Example:
{
"el": "f-editor-dialog",
"w": "6",
"attr": {
":value": "editorState",
"label": "Noi dung mo rong",
":width": 1000,
"imageapi": "/api/editor/upload"
}
}f-dialog
- Props (source):
value,data,dataOut,title,controls,watch,button,disableValidate,width,openAction - Emit:
inputto open/close dialogupdate:data-outwhen default data is preparedupdate:datawhen button hasgetoutdata=true- Behavior:
- dynamically builds form from
controls - supports local dialog watchers via
watch - supports
openActionwhen dialog opens - Example:
{
"el": "f-dialog",
"w": "12",
"attr": {
"v-model": "dialogOpen",
":data.sync": "dialogData",
":data-out.sync": "dialogOut",
"title": "Cap nhat",
":controls": "dialogControls",
":button": "dialogButtons",
":open-action": { "CALL": "loadDialogDefaults" }
}
}f-pdfmake
- Props (source):
data - Behavior: renders PDF in iframe, rerenders on deep
datachanges - Example:
{
"el": "f-pdfmake",
"w": "12",
"attr": {
":data": "pdfDefinition",
"height": "550"
}
}f-excel-reader
- Props (source):
dateFormat,headerFormat,rawFormat,action,label - Emit:
inputwith parsed sheet JSON - Behavior: reads first sheet, optional
CALL(action)after parse - Example:
{
"el": "f-excel-reader",
"w": "4",
"attr": {
"label": "Doc Excel",
":header-format": 1,
":raw-format": false,
":action": { "CALL": "handleExcelImported" },
"v-model": "excelRows"
}
}Practical Notes
1. Most f-* components pass unknown attrs through v-bind=\"$attrs\"; Vuetify attrs can still work if child is a Vuetify control. 2. Apply literal-string rule only when API/path strings are passed inside Action IN mapping. 3. For table-specific components, use references/component-table.md.
Controls Patterns & Logic
Guide to defining logic and layout in module.json.
1. Component Actions (data)
Actions are the "methods" of your module. They handle APIs, dialogs, and logic flow.
Standard API Call
"apiLoadData": {
"API": "/api/controller/action", // Endpoint
"IN": { // Input Mapping
"Page": 1, // Static value
"Search": "vueData.searchText", // Dynamic state
"ID": "item.ID" // Context item (loops)
},
"OUT": "items", // Output state variable
"CALLBACK": { // Chained action
"MESS": "Data loaded!",
"CALL": "anotherAction"
}
}Literal String Values (Important)
Apply this rule only for values passed inside action IN mapping. If a string has no spaces, FUI can treat it like an expression/variable in IN. When you need a plain literal string, force it with:
- Backtick prefix: `
"myValue"`(or closed"myValue"`) - Single quotes inside JSON string:
"'myValue'"
Examples:
{
"openDialog": {
"FUN": "openWindow",
"IN": {
"id": "`winUser",
"url": "'/fp/module?mid=123'"
}
}
}For component props in attr without :, value is already a plain string prop and usually does not need wrapping.
Conditional Logic
"checkStatus": {
"IF": "vueData.status === 1",
"THEN": { "MESS": "Active" },
"ELSE": { "MESS": "Inactive" }
}Confirmation
"deleteItem": {
"CONFIRM": "Are you sure you want to delete this item?",
"API": "/api/delete",
"IN": { "id": "item.id" },
"CALLBACK": { "CALL": "apiLoadData" }
}2. Watchers (watch)
Trigger actions automatically when data changes.
For production-grade cascading and filter watcher design, see watcher-patterns.md.
"watch": {
"searchText": { // Variable to watch
"CALL": "apiLoadData" // Action to run (debounce is auto-handled by FUI usually)
},
"selectedGroup": {
"CALL": "apiLoadUsers"
}
}3. Controls Layout (controls)
MANDATORY: All UI elements MUST be nested within a Grid System structure: Container > Row > Col > Element. Do NOT place elements directly at the root.
Basic Grid Structure
{
"prop": "fluid grid-list-md", // CONTAINER (v-container)
"rows": [
{
"prop": "row wrap", // ROW (v-layout)
"cols": [
{
"w": "6", // COLUMN (v-flex) - Nested Element goes here
"el": "v-text-field",
"attr": { ... }
},
{
"w": "6",
"el": "v-btn",
"innerHTML": "Submit"
}
]
}
]
}innerHTML Supports {{ }}
Use Vue interpolation in innerHTML for text pulled from data.
{
"el": "div",
"w": "12",
"innerHTML": "Xin chao {{formData.fullName}}"
}Responsive Forms
Control form width dynamically using breakpoints.
Step 1: Define Config
"data": [
{
"configForm": {
"xs": { "width": "100%" }, // Mobile
"md": { "width": "500px" } // Desktop
}
}
]Step 2: Apply to Dialog/Form
"attr": {
":width": "$vuetify.breakpoint.mdAndUp ? configForm.md.width : configForm.xs.width"
}4. Common Element Patterns
- HTML Content: Use
innerHTMLfor text or simple HTML. - Events:
v-on:click,v-on:change. wrap logic inCALL(). - Visibility: Use
v-if(pre-render) orv-show(CSS toggle). - Loops:
v-foris rarely used directly inmodule.json. Usef-tablefor lists or recursive partials.
Default Functions
This document references the global utility functions and variables defined in defaultfunction.js.
Global Variables
| Variable | Description |
|---|---|
$isMobile | Boolean indicating if the user agent matches a mobile device. |
$isPhone | Boolean indicating if the user agent matches a phone device (excluding tablets). |
$isApple | Boolean indicating if the user agent is an Apple device (iPad, iPhone, iPod). |
Classes
Loader
A utility class for dynamically loading scripts.
Methods:
require(scripts, callback): Loads an array of script URLs and executes the callback when all are loaded.loaded(evt): Internal method called when a script loads.writeScript(src): Internal method to inject a script tag.
Utility Functions
extractHostname(url)
Extracts the hostname from a given URL.
- Parameters:
url(String) - Returns: Hostname (String)
getDomainWithoutSubdomain(url)
Extracts the domain name without subdomains from a URL.
- Parameters:
url(String) - Returns: Domain (String)
loadScripts(scriptsArray, callbackFunc)
Helper function to use the Loader class to load multiple scripts.
- Parameters:
scriptsArray(Array<String>): List of script URLs.callbackFunc(Function): Function to execute after loading.
pushRouter(router)
Pushes a new state to the browser history.
- Parameters:
router(String) - The new path.
CALL(obj, includeData)
Alias for runAction.
- Parameters:
obj(Object): The action object.includeData(Object): Additional data to merge into context.
windowSendMessage(window, cmd, data)
Sends a postMessage to a specific window or iframe.
- Parameters:
window(Object|String): Target window object or ID (prefixed with#for ID, or#PARENT).cmd(String): Command name.data(Object): Data payload.
appCommand(cmdData)
Sends a message to the native app wrapper (ReactNative, WebKit, etc.).
- Parameters:
cmdData(Object)
buildHeader(obj, mapCol)
Builds a header array for data tables based on an object key set or specification.
- Parameters:
obj(Array): Data source.mapCol(Object): Optional mapping for headers.- Returns: Array of header objects.
fillData(obj)
Fills data from a source array into a destination object based on a key and mapping.
- Parameters:
obj(Object) - { src, des, key, map }
groupBy(arrayObj, groupField, sumArr)
Groups an array of objects by a field and sums specified columns.
- Parameters:
arrayObj(Array): Input array.groupField(String): Field to group by.sumArr(Array<String>): Fields to sum.- Returns: Grouped array with counts and sums.
chartDataBuild(obj)
Formats data for Chart.js based on the dataChart property.
- Parameters:
obj(Object) - Returns: Formatted chart data array.
chartQABuild(obj)
Formats data for QA charts specifically.
- Parameters:
obj(Object) - Returns: Formatted chart data.
confirm(obj)
Displays a confirmation dialog using $.confirm.
- Parameters:
obj(Object) - { title, message, action, cancel, icon, type }
showMessage(obj)
Displays a message dialog using $.dialog.
- Parameters:
obj(Object) - { title, message, icon, type, onclose }
rightTest(rightObject)
Checks if the current user has the required rights specified in rightObject.
- Parameters:
rightObject(Object) - Key-value pairs of required rights. - Returns: Boolean.
openWindow(obj)
Opens a new FUI window/dialog.
- Parameters:
obj(Object|String) - Window configuration or URL string. id: Window ID (optional, generated if missing).url: Content URL.title: Window title.
redirect(obj, query)
Redirects the current page.
- Parameters:
obj(Object|String): Target URL or configuration object.query(Boolean): Whether to preserve current params.
reload()
Reloads the current page.
findInArray(obj)
Finds an item in an array matching a value.
- Parameters:
obj(Object) - { array, value } - Returns: Found item.
fixURL(url)
Prepends the API domain if the URL is relative.
- Parameters:
url(String) - Returns: Absolute URL.
stringAttrToJson(str, removeVueEvent)
Parses a string of HTML attributes into a JSON object.
- Parameters:
str(String): HTML attribute string.removeVueEvent(Boolean): If true, removes Vue events (v-*, @, :).- Returns: JSON Object.
json_data_parse(obj, level)
Recursively parses json_data: prefixed string values in an object structure into JSON.
- Parameters:
obj(Object): The object to parse.level(Number): Recursion depth.
ajaxCALL(URL, DATA, callBack, errorCallBack, header)
Performs an AJAX POST request.
- Parameters:
URL(String): Endpoint URL.DATA(Object): Request payload.callBack(Function): Success callback.errorCallBack(Function): Error callback.header(Object): Custom headers.
capacityText(numb)
Formats a number of bytes into Kb or Mb.
- Parameters:
numb(Number) - Returns: Formatted string.
generateID()
Generates a random unique ID string.
- Returns: String.
printPDF(obj)
Generates and prints/downloads a PDF using pdfMake.
- Parameters:
obj(Object) - { data, download, viewer, callBack }
colorLib(color)
Utility for color manipulation (hex, rgb, hsl).
- Parameters:
color(String) - Returns: Object with methods
hexString,rgbString,hslString,lighten,darken,alpha.
transparentize(value, opacity)
Creates a transparent version of a color.
- Parameters:
value(String): Color code.opacity(Number): Opacity (0-1).- Returns: RGBA string.
jsonToExcel(Obj)
Exports JSON data to an Excel file using ExcelJS.
- Parameters:
Obj(Object) - { data, filename, worksheet, sheets }
FastProject (Core Framework)
This document references the core framework logic defined in fastproject.js. This script is responsible for initializing the application, building the UI from JSON configurations, and handling the logic engine.
Global State
vueData
The central reactive state object for the application.
p_domain,p_routers,p_params: Routing and environment info.user: Current user information.v_Set: Application settings (title, menu, attributes).v_Loading: Global loading state.
vueOBJ
The Vue instance configuration object.
Core Functions
loadModuleInfo()
Initializes the module by: 1. Loading project and module settings. 2. Building URL parameters. 3. Fetching user info (if configured). 4. Calling createModuleDom().
createModuleDom()
Builds the Vue application structure: 1. Executes initial data actions ($moduleUI.data). 2. Generates the HTML structure using buildModuleUI. 3. Initializes Vue watchers and WebSocket connections. 4. Mounts the Vue instance.
buildModuleUI(controlsList, target)
Recursively builds the DOM structure from a JSON list of controls.
- Parameters:
controlsList(Array): List of control definitions.target(Object): Target object for data binding (usuallyvueData).- Returns: jQuery object containing the generated HTML.
buildControl(controlsList, flex, target)
Helper function for buildModuleUI to generate individual controls.
- Supports grid layout (
v-layout,v-flex) and nested components. - Handles attribute binding and event mapping.
runAction(obj, includeData)
The central logic engine. Executes a sequence of actions defined in JSON.
- Parameters:
obj(Object|Array): The action(s) to execute.includeData(Object): Context data.- Supported Action Types:
CONFIRM: Show confirmation dialog.API: Call an API.CALL: Recursive call to another action.EXE: Execute raw JavaScript.FUN: Call a global function.IF/THEN/ELSE: Conditional logic.MESS/MESSBOX: Show messages.IN/OUT: Data mapping.
vueAction(objAction, includeData, callBack)
Internal function used by runAction to execute a single action step.
callAPI(objApi, callBack, includeData)
Executes an API call defined in a JSON action object.
- Handles
BEFORE,AFTERactions, and success/error callbacks. - Automatically maps response data to
vueDataifOUTis specified.
mapData(map, target, src, includeData)
Maps data from a source object to a target object based on a mapping definition.
- Supports cross-window data mapping using
#PARENTor window IDs. - Parameters:
map(Object|String): Mapping definition.target(Object): Destination object.src(Object): Source object.
getVueData(key, src)
Resolves a value from a key string, supporting deep paths and template syntax (e.g., {{user.Name}}).
bindData(obj)
Resolves all dynamic values within an object.
createWatch(obj)
Sets up Vue watchers based on a JSON configuration.
- Supports deep watching.
- Executes actions when watched values change.
loginFUN()
Handles user login redirection.
- Opens a login window or redirects to the login page.
errorMess(code, message)
Displays standard error messages based on HTTP status codes.
- Parameters:
code(String|Number): HTTP status code.message(String): Custom error message.
Module Structure (Canonical for Workspace and Chat)
Use this structure when creating, refactoring, or reviewing a FUI module.
- In editor or extension contexts with workspace access, apply it as the real local folder structure.
- In chat or agent-chat contexts, use it to virtualize the same module structure and organize the response, without assuming the files already exist on disk.
Canonical Local Structure
<module-name>/
|-- _info.json # Required: module metadata
|-- module.json # Required: data/watch/controls/set
|-- script.js # Recommended: helper logic for FUN/EXE/chart/transform
|-- dependencies.json # Recommended: external js/css declarations
|-- header.html # Recommended: all module/component CSS lives here
|-- body.html # Optional: additional body markup
|-- components/ # Optional: custom Vue components
| |-- _components.json # Required when using custom components
| `-- uc-*.vue # Custom components (prefix uc-)Chat-Safe Usage
When working without local workspace access:
1. Present this tree as the canonical module layout, not as a claimed filesystem state. 2. Return file contents in separate code blocks or clearly labeled sections. 3. Ask the user to paste _info.json, module.json, script.js, or component files when a review or patch depends on existing code. 4. Scope recommendations to the files actually provided instead of inventing unseen surrounding files.
Required vs Optional
1. Required:
_info.jsonmodule.json
2. Recommended:
script.jsdependencies.json
3. Optional by use-case:
header.htmlbody.htmlcomponents/(with_components.jsonanduc-*.vue)
File Responsibilities
module.json (core)
- Keep all runtime config in
data,watch,controls,set. - Keep
controlsunder FUI grid wrapper (container > rows > cols). - Keep business actions in
dataand trigger withCALL.
script.js (helper logic)
- Move complex transforms, chart builders, debounce helpers, parsing logic here.
- Expose functions for
FUNactions or template helpers. - Avoid large inline
EXEblocks inmodule.jsonwhen reusable function is possible.
_info.json (metadata)
- Keep
ProjectID,ModuleID,ModuleName,Frameworkaccurate. - Keep update/load metadata current when publishing workflow requires it.
dependencies.json
- Declare external library dependencies used by module/components.
- Keep only used dependencies to avoid bloat.
components/_components.json
- Register
uc-*components. - Follow upsert rule:
- Before publish: may only have
comName. - After publish/sync:
comIDis server-managed. - Do not handcraft random
comID.
components/uc-*.vue
- Use
uc-prefix for custom components. - Keep component-specific UI complexity here instead of bloating
module.json. - Prefer reusable component contracts: props for input, emits for output, slots for extensibility.
- Avoid baking page-specific API calls or route logic into the component unless that coupling is intentional and unavoidable.
- Never place
<style>or<style scoped>blocks inside the component. Move all CSS toheader.html. - Never use backtick template strings inside the component
<template>.
Naming and Prefix Rules
1. Module folder name:
- Use the same module id naming used by project conventions.
2. Custom component file:
- Use
uc-*.vuekebab-case.
3. Component usage in module.json:
- Use
el: "uc-..."for custom components. - Keep prop binding in
attrwith kebab-case prop names.
Practical Patterns
Pattern A: Simple module (no custom component)
Use:
_info.jsonmodule.jsonscript.jsdependencies.json
Skip components/ unless needed.
Pattern B: Dashboard/report module (recommended split)
Use:
module.jsonfor filters + action orchestrationscript.jsfor conversion/chart utilitiescomponents/uc-*.vuefor chart/table dashboard presentationheader.htmlfor all module-scoped and component-scoped styles
This pattern matches large report modules and keeps module.json maintainable.
Review Checklist (Structure)
1. If the module files are available, does module root contain _info.json and module.json? 2. Is heavy logic moved to script.js instead of oversized EXE? 3. If custom UI exists, is it moved to components/uc-*.vue? 4. If components/ exists, does _components.json register them correctly? 5. Are dependencies listed in dependencies.json and actually used? 6. Are optional files (header.html, body.html) used intentionally, not as dumps?
Module Assessment & Quality Assurance
This guide defines the protocol for critiquing, assessing, and optimizing FUI modules. Use this skill when the user asks to "Review", "Assess", or "Critique" a module.
Assessment Protocol
When assessing a module, perform the following 3-step analysis:
1. Code Quality & Architecture Review
- Structure: Does the module follow the standard directory structure (
module.json,script.js,_info.json)? - JSON Standards: Are
module.jsonkeys valid? Areattrandcolused correctly? - Separation of Concerns: Is complex logic moved to
script.jsinstead of cluttering JSON? - Clean Code: Are variable names consistent? Is dead code removed?
2. Logic & Edge Case Analysis
Identify potential failures in real-world scenarios.
- Data Validation: Are required fields checked before submission? What happens if fields are empty?
- State Management: Does the module handle loading states? Is there a risk of race conditions?
- User Flow:
- _Scenario_: User clicks "Back" mid-process. Does data persist?
- _Scenario_: User inputs invalid numbers/dates. Is it handled gracefully?
- Security: Are permissions checked? Is sensitive data exposed?
3. Best Practice Proposals
Propose optimization based on FUI standards.
- Performance: Can big lists become virtualized? Are there unnecessary re-renders?
- UX: Can steps be combined? Are error messages clear?
- Maintainability: Can repetitive validation be refactored into a helper function?
Output Format
When delivering an assessment, structure your response as follows:
## Module Assessment Report
### 🚨 Critical Issues
- [Logic] Missing validation on "Amount" field allows negative numbers.
- [UX] "Submit" button remains active during loading, allowing double submission.
### ⚠️ Improvements
- [Architecture] Move the 50-line validation logic from JSON to `script.js`.
- [UI] Use `f-date` instead of text input for date fields.
### 💡 Best Practice Proposals
- **Refactor**: Create a `validateForm()` function to handle all checks centrally.
- **UX**: Add a progress spinner when calling `apiLuu`.
### 🧪 Edge Cases to Verify
1. User deletes a row from `dsDienGiai` then tries to submit.
2. User uploads a file larger than 20MB.FUI Script Map (Quick Lookup)
Mục tiêu: tra cứu nhanh "hàm nào dùng để làm gì" theo từng file runtime trong scripts/.
Lưu ý dùng đúng chuẩn FUI:
- Metadata chính là
module.json(không dùngcontrols.json). - Logic action gọi qua
CALL(vueData.actionName)hoặc Action Protocol. - Layout trong
controlsluôn theo grid wrappercontainer > rows > cols. - Event trong JSON dùng
v-on:...(không dùng@...). - Literal forcing (`
`or'...') is for ActionIN` mapping, not normal static component props.
Table of Contents
- 1) scripts/fastproject.js
- 2) scripts/defaultfunction.js
- 3) scripts/component.js
- 4) scripts/componentTable.js
- 5) Tra cứu theo tác vụ
1) scripts/fastproject.js
Vai trò: core engine để khởi tạo module, dựng DOM từ module.json.controls, chạy action, map data và gọi API.
Luồng khởi tạo chính: 1. $(document).ready -> loadModuleInfo() 2. loadModuleInfo() -> merge config + đọc URL/user info 3. createModuleDom() -> runAction($moduleUI.data) + render controls 4. createWatch() -> gắn watcher từ $moduleUI.watch 5. Vue instance mount và module chạy
| Line | Hàm | Dùng để làm gì |
|---|---|---|
| 65 | buildParamURL() | Đọc query string và đẩy params vào vueData. |
| 77 | loadModuleInfo() | Nạp config project/module, user info, chuẩn bị tạo app. |
| 122 | createModuleDom() | Tạo layout Vue chính, render controls, init watch. |
| 210 | buildModuleUI(controlsList, target) | Recursively build UI từ JSON controls. |
| 233 | buildControl(controlsList, flex, target) | Render từng control, bind attr/event/import. |
| 299 | addImport(importArr) | Nạp script/css phụ thuộc theo control. |
| 310 | createWatch_Data(obj) | Khởi tạo dữ liệu cho watch config. |
| 320 | createWatch(obj) | Gắn watcher Vue theo config watch. |
| 345 | buildVisualValForObj(controlAttr, target) | Bind giá trị động vào attr control. |
| 373 | runAction(obj, includeData) | Action engine tổng: chạy object/array action. |
| 407 | vueAction(objAction, includeData, callBack) | Chạy 1 action cụ thể (IF/API/CALL/EXE/...). |
| 485 | mapData(map, target, src, includeData) | Map dữ liệu giữa object theo rule FUI. |
| 527 | defProp(obj) | Chuẩn hoá property object theo context. |
| 537 | bindData(obj) | Resolve biến động (string/object) từ context hiện tại. |
| 542 | getVueData(key, src) | Lấy value theo path (vueData.x.y, item.x, template...). Literal forcing is for IN mapping; static component props without : are already plain strings. |
| 578 | setValue(target, key, value, srcData, index) | Gán dữ liệu theo path vào target object/window. |
| 612 | bindDataToString(str, data) | Resolve template string với dữ liệu runtime. |
| 619 | templateCompiled(str, data) | Compile template lodash-style cho chuỗi động. |
| 624 | getWindowData(value) | Lấy dữ liệu từ window/iframe context. |
| 638 | setWindowData(key, value) | Set dữ liệu cho window/iframe context. |
| 657 | runFunction(obj, callback) | Gọi function JS đã khai báo theo action config. |
| 665 | tableActionEvent(ctrl) | Xử lý action event liên quan table/control. |
| 687 | callAPI(objApi, callBack, includeData) | Gọi API theo cấu hình action (IN/OUT/CALLBACK). |
| 743 | loginFUN() | Luồng đăng nhập/chuyển hướng login. |
| 761 | errorMess(code, message) | Hiển thị lỗi chuẩn theo status/message. |
2) scripts/defaultfunction.js
Vai trò: utility layer dùng chung cho runtime (router, messaging, data transform, AJAX, export, auth, websocket...).
| Line | Hàm | Dùng để làm gì |
|---|---|---|
| 35 | extractHostname(url) | Lấy hostname từ URL. |
| 53 | getDomainWithoutSubdomain(url) | Lấy domain gốc không subdomain. |
| 62 | loadScripts(scriptsArray, callbackFunc) | Nạp nhiều script động rồi callback. |
| 75 | pushRouter(router) | Push route vào browser history. |
| 82 | CALL(obj, includeData) | Alias gọi runAction(...). |
| 86 | windowSendMessage(window, cmd, data) | postMessage sang parent/window/iframe. |
| 100 | appCommand(cmdData) | Gửi lệnh về native wrapper (RN/WebKit). |
| 106 | buildHeader(obj, mapCol) | Tạo header table từ dataset/map cột. |
| 130 | fillData(obj) | Fill/map dữ liệu từ source sang destination. |
| 162 | groupBy(arrayObj, groupField, sumArr) | Group mảng theo field + cộng tổng cột. |
| 188 | chartDataBuild(obj) | Chuẩn hoá data cho chart tổng quát. |
| 219 | chartQABuild(obj) | Build data chart chuyên QA/report. |
| 239 | confirm(obj) | Hiển thị dialog confirm. |
| 259 | showMessage(obj) | Hiển thị dialog/toast message. |
| 283 | rightTest(rightObject) | Kiểm tra quyền theo user rights. |
| 301 | openWindow(obj) | Mở popup/dialog window FUI. |
| 326 | redirect(obj, query) | Điều hướng trang theo URL/config. |
| 339 | reload() | Reload trang hiện tại. |
| 343 | findInArray(obj) | Tìm phần tử theo điều kiện giá trị. |
| 348 | fixURL(url) | Chuẩn hoá URL (ghép domain API nếu cần). |
| 354 | stringAttrToJson(str, removeVueEvent) | Parse chuỗi attr HTML thành JSON object. |
| 371 | json_data_parse(obj, level) | Parse đệ quy field có prefix json_data:. |
| 407 | ajaxCALL(URL, DATA, callBack, errorCallBack, header) | Gọi AJAX POST helper. |
| 449 | capacityText(numb) | Format dung lượng bytes -> KB/MB text. |
| 454 | generateID() | Sinh ID ngẫu nhiên. |
| 467 | printPDF(obj) | Render/in/download PDF bằng pdfMake. |
| 499 | colorLib(color) | Bộ công cụ convert/manipulate màu. |
| 645 | transparentize(value, opacity) | Tạo màu trong suốt (alpha). |
| 745 | jsonToExcel(Obj) | Xuất dữ liệu JSON ra Excel. |
| 841 | copyToClipboard(textToCopy) | Copy text vào clipboard. |
| 865 | fileNameClear(fname) | Làm sạch tên file không hợp lệ. |
| 870 | hashCode(s) | Sinh hash code từ string. |
| 880 | getCookie(cname) | Đọc cookie theo tên. |
| 895 | setCookie(name, value, days, domain) | Set cookie có expiry/domain. |
| 909 | logout(cookiesName, domain, url) | Xoá cookie và logout/redirect. |
| 922 | webSocketJoinGroup(groupObj, timeout, callBackFunc) | Join WS group theo payload. |
| 946 | webSocketConnection(wsURL, WS, wsState) | Khởi tạo/duy trì WS connection. |
| 987 | webSocket_Send(obj) | Gửi payload qua websocket hiện tại. |
3) scripts/component.js
Vai trò: đăng ký FUI base components và các component nghiệp vụ (dialog, editor, PDF, excel, chart, media...).
Ghi chú:
- File này cũng chứa
defaultControlAttrđể set attr mặc định theo từngel. - Nhiều component yêu cầu import ngoài (CKEditor, pdfMake, cropper, chart, ...).
| Line | Component | Dùng để làm gì |
|---|---|---|
| 317 | f-label | Hiển thị label/text theo format FUI. |
| 332 | f-box | Khối hiển thị dữ liệu dạng box/card đơn giản. |
| 390 | f-header | Header block nhẹ cho section/module. |
| 399 | f-title | Title component chuẩn UI. |
| 408 | f-radiobox | Radio group wrapper. |
| 438 | f-menu | Menu list/navigation control. |
| 485 | f-search | Input tìm kiếm dùng lại. |
| 549 | f-button | Button wrapper chuẩn attr FUI. |
| 620 | f-slider | Slider input/display. |
| 631 | f-file-upload | Upload file cơ bản. |
| 781 | f-qrcode | Render QR code. |
| 856 | f-qrcode-reader | Quét/đọc QR code. |
| 927 | f-image-update | Dialog crop/rotate/upload ảnh. |
| 1174 | f-date | Date picker wrapper. |
| 1447 | f-time | Time picker wrapper. |
| 1652 | f-time-counter | Counter/countdown/clock hiển thị thời gian. |
| 1788 | fp-profile | User profile menu trên header. |
| 2033 | header-bar | Thanh header/menu chính của app/module. |
| 2290 | f-chart | Wrapper chart. |
| 2401 | f-chart-data-viewer | Viewer cho dữ liệu chart. |
| 2429 | f-window | Dynamic window/dialog host. |
| 2493 | f-editor | WYSIWYG editor wrapper (CKEditor). |
| 2631 | f-editor-dialog | Dialog chứa editor toàn màn hình. |
| 2727 | f-dialog | Dialog dynamic dựng form từ controls. |
| 2939 | f-pdfmake | Viewer/render PDF bằng printPDF. |
| 2974 | f-excel-reader | Upload/đọc Excel, emit action xử lý data. |
4) scripts/componentTable.js
Vai trò: table system cho FUI, gồm component table chính và cell renderer t-*.
| Line | Component | Dùng để làm gì |
|---|---|---|
| 2 | f-table | Data table chính: search, select, CRUD (update-api, update-form), export. |
| 366 | f-table-view | Phiên bản table view/read-only đơn giản hơn. |
| 448 | t-html | Render ô dạng HTML. |
| 452 | t-label | Render text label thường. |
| 456 | t-num | Render/format số. |
| 474 | t-time | Render/format thời gian. |
| 484 | t-boolean | Render bool bằng icon/màu. |
| 505 | t-check | Cell checkbox có thể tương tác. |
| 533 | t-text | Cell text input inline. |
| 555 | t-select | Cell select inline. |
| 576 | t-combobox | Cell combobox/autocomplete inline. |
| 648 | t-menu | Cell menu action. |
| 686 | t-link | Cell link điều hướng/open dialog. |
| 710 | t-button | Cell button action custom. |
5) Tra cứu theo tác vụ
| Tác vụ cần làm | Mở file trước | Xem mục chính |
|---|---|---|
| Module không render đúng layout | scripts/fastproject.js | buildModuleUI, buildControl, bindData. |
| Action không chạy hoặc chạy sai nhánh | scripts/fastproject.js | runAction, vueAction, mapData, getVueData. |
API không ra dữ liệu OUT | scripts/fastproject.js + scripts/defaultfunction.js | callAPI, fixURL, ajaxCALL. |
| Table CRUD lỗi Add/Edit/Delete | scripts/componentTable.js | f-table + update-api, update-form. |
| Dialog dynamic không bind dữ liệu | scripts/component.js | f-dialog, buildModuleUI (ở fastproject). |
| Upload ảnh, crop, lưu file | scripts/component.js | f-image-update. |
| Export Excel/PDF | scripts/defaultfunction.js + scripts/component.js | jsonToExcel, printPDF, f-pdfmake, f-excel-reader. |
| Sự cố đăng nhập/quyền/menu | scripts/fastproject.js + scripts/defaultfunction.js | loadModuleInfo, loginFUN, rightTest. |
UI Templates Guide
Use templates from examples/ as starting points for common UI patterns.
Available Templates
form-basic.json
When to use: Simple data entry forms with text fields, selects, checkboxes.
examples/form-basic.jsonKey sections to modify:
formData: Change field names and default valuessubmitForm.API: Set your API endpointsubmitForm.IN: Map form fields to API params- Add/remove form controls in
controls
---
table-crud.json
When to use: Data tables with Create, Read, Update, Delete operations.
examples/table-crud.jsonKey sections to modify:
apiLoadData.API: Your list API endpointheaders: Column definitionsupdate-form: Edit form fieldsupdate-api.new/edit/delete: CRUD API endpoints
---
dialog-form.json
When to use: Modal popups with forms (e.g., quick edit, confirmation with input).
examples/dialog-form.jsonKey sections to modify:
dialogData: Fields for your formf-dialog.controls: Form controls inside dialogf-dialog.button[].action: Submit API and callbacks
Usage Pattern
1. Copy template content to your module.json 2. Replace placeholder APIs with real endpoints 3. Adjust field names and labels 4. Add to existing data and controls arrays
Watcher Patterns (FUI Best Practice)
Hướng này dùng cho các case:
- Form phụ thuộc nhiều cấp (Tỉnh/Thành -> Quận/Huyện -> Phường/Xã)
- Bộ lọc theo thuộc tính (status, loại, ngày, keyword, ...)
Mục tiêu:
- Đúng cấu trúc FUI (
module.json:data,watch,controls,set) - Tránh vòng lặp watcher
- Giảm gọi API thừa
- Dễ bảo trì khi module lớn
1. Nguyên tắc chuẩn
1. Chỉ watch key đầu vào nhỏ nhất
- Watch ID hoặc field filter (
provinceID,districtID,filter.status), không watch cả object lớn nếu không cần.
2. Tách action theo tầng
handleProvinceChangechỉ reset dữ liệu phụ thuộc và gọiapiLoadDistricts.handleDistrictChangechỉ reset cấp dưới và gọiapiLoadWards.applyFiltersgom logic tải list cuối.
3. Dùng deep-watch có chọn lọc
- Chỉ dùng cho object filter nhỏ (ví dụ
filter). - Không deep-watch mảng lớn như
items,tableData.
4. Không để watcher tự tạo loop
- Không watch biến output của chính action đó.
- Không watch
itemsrồi trong callback lại ghiitems.
5. Gắn điều kiện trước khi gọi API
- 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).
6. Tách debounce/race-control sang script.js
- Watcher giữ vai trò điều phối.
- Debounce hoặc chống response cũ ghi đè response mới đặt trong helper JS.
7. Literal string chỉ ép khi truyền qua IN
- Trong action có
IN, chuỗi không có khoảng trắng có thể bị core hiểu là expression. - Dùng `
value`hoặc"'value'"cho các giá trị text cần giữ nguyên trongIN`.
2. Pattern A: Cascading địa giới hành chính
{
"data": [
{
"formData": {
"provinceID": null,
"districtID": null,
"wardID": null
},
"provinceList": [],
"districtList": [],
"wardList": []
},
{
"apiLoadProvinces": {
"API": "/api/location/provinces",
"OUT": "provinceList"
},
"apiLoadDistricts": {
"API": "/api/location/districts",
"IN": { "ProvinceID": "formData.provinceID" },
"OUT": "districtList"
},
"apiLoadWards": {
"API": "/api/location/wards",
"IN": { "DistrictID": "formData.districtID" },
"OUT": "wardList"
}
},
{
"handleProvinceChange": [
{
"districtList": [],
"wardList": [],
"formData.districtID": null,
"formData.wardID": null
},
{
"IF": "formData.provinceID",
"THEN": { "CALL": "apiLoadDistricts" }
}
],
"handleDistrictChange": [
{
"wardList": [],
"formData.wardID": null
},
{
"IF": "formData.districtID",
"THEN": { "CALL": "apiLoadWards" }
}
]
},
{
"CALL": "apiLoadProvinces"
}
],
"watch": {
"formData.provinceID": { "CALL": "handleProvinceChange" },
"formData.districtID": { "CALL": "handleDistrictChange" }
}
}Điểm chính:
- Watch theo key cụ thể.
- Reset cấp dưới trước khi gọi API cấp dưới.
- Có điều kiện tránh gọi API khi null.
3. Pattern B: Filter object + deep-watch
{
"data": [
{
"filter": {
"keyword": "",
"status": null,
"fromDate": null,
"toDate": null
},
"items": []
},
{
"apiLoadItems": {
"API": "/api/items/search",
"IN": {
"Keyword": "filter.keyword",
"Status": "filter.status",
"FromDate": "filter.fromDate",
"ToDate": "filter.toDate"
},
"OUT": "items"
}
},
{
"handleFilterChanged": {
"CALL": "apiLoadItems"
}
}
],
"watch": {
"deep-watch": {
"filter": { "CALL": "handleFilterChanged" }
}
}
}Khi dùng pattern này:
filternên nhỏ và ổn định.- Nếu có text search nhập liên tục, nên debounce trong
script.js.
4. Debounce khuyến nghị (script.js)
var filterTimer = null;
function debounceFilter(input) {
var callActionName = input && input.callActionName;
var waitMs = input && input.waitMs;
clearTimeout(filterTimer);
filterTimer = setTimeout(function () {
CALL(vueData[callActionName]);
}, waitMs || 350);
}Ví dụ action trong module.json:
{
"handleFilterChanged": {
"FUN": "debounceFilter",
"IN": {
"callActionName": "'apiLoadItems'",
"waitMs": 350
}
}
}5. Checklist QA cho watcher
1. Watch key có đủ nhỏ chưa (ID/field thay vì object lớn)? 2. Có reset đúng dữ liệu phụ thuộc trước khi gọi API? 3. Có guard IF trước API khi input null/rỗng? 4. Có nguy cơ loop watcher không? 5. Có cần debounce cho input text không? 6. Nếu action có IN, đã áp dụng literal string rule cho giá trị text không khoảng trắng chưa?