# NGX View Builder: JSON authoring reference > NGX View Builder is a visual builder for complete Angular views: forms, dashboards, data tables, and guided flows are designed in a drag-and-drop builder, stored as JSON, and rendered natively in an Angular app. This file is the complete AI reference for AUTHORING NGX View Builder structure JSON: the rules of engagement, the layout model, every supported property, and verified end-to-end examples. It is self-contained on purpose, so it can be pasted whole into a chat as system material. Read it as a specification, not as inspiration. Element types, property names and object shapes are exact and case-sensitive; every JSON block here is machine-checked against the library source. A property that does not appear in this file does not exist, and writing one is silently ignored at runtime, so the view ships broken with no error anywhere. For host-side integration (embedding the builder, the runtime API, events, theming, plugins) see https://ngxviewbuilder.io/llms-full.txt instead. --- ## Live builder command API Source: https://ngxviewbuilder.io/ai/command-api # Live builder command API Everything else in this section teaches you to **write** structure JSON. This page is about the other mode: when a builder is open in a browser you can reach, you can **read and edit the live view directly**, one JSON command at a time, and see the result immediately. ## Detect the mode first The live mode arrives as an MCP server. If your client has the `nvb_*` tools, call `nvb_status`. | Result | What it means | What to do | | --- | --- | --- | | `paired` and `builder_attached` are `true` | A builder is open and the API is armed | Drive it with commands. Do not hand back raw JSON for a person to paste. | | `builder_attached` is `false` | The tab is connected but the builder is not on screen | Ask the user to open the builder. | | `browser_connected` is `false` | The paired tab was closed or lost its socket | Ask the user to reopen the builder and re-pair. | | An error mentioning a pair code | Nothing is paired yet | Ask the user for the code shown in the builder settings, under AI access (MCP). | | No `nvb_*` tools at all | Runtime only host, or no MCP server configured | Fall back to authoring structure JSON as the rest of this section describes. | The bridge is open only while the builder component is mounted. It closes when the user leaves the builder, so re-check before a long sequence. ## Bootstrap sequence Run these before writing anything. They cost one round trip and remove almost every source of error. ```text nvb_status 1. is the door open nvb_get_instructions 2. the contract as text, plus the command catalog nvb_describe_element_types 3. real element types and their real property keys nvb_get_tree 4. what already exists, and the row indexes you need ``` `nvb_get_instructions` is the fastest way in: it states what the API is, the working order, the rules, the capabilities this builder has, and the templates already in the library. It returns `help()` in the same call, so `help.guidelines` carries the rules on their own and `help.capabilities` tells you which optional features are present. `help()` is authoritative and versioned. If it disagrees with this page, follow `help()`. **Never guess a property name.** `describeElementTypes(type)` returns the exact keys the builder accepts for that type. Guessing produces a warning and a property the runtime ignores, which is worse than an error because it looks like it worked. ## The one write door ```js const result = await api.execute(commandOrArray, options); ``` Over MCP that is `nvb_execute`, with the array under `commands` and the options as flat arguments (`dry_run`, `return_tree`, `return_structure`). The snippets below show the command shapes, which are the same either way. Hard rules: 1. **An array is atomic.** If one command fails, none are applied. Prefer one batch over a sequence of single calls, so a partial build cannot happen. 2. **Use `{ dryRun: true }` first** for anything large or destructive. It runs every check and reports `changed` without touching the view. 3. **Read `warnings`, not just `errors`.** `ok: true` with warnings means something landed that the builder does not recognise. 4. **Act on `hint`.** Errors carry a concrete correction. One retry using the hint should succeed; if it does not, stop and ask rather than looping. 5. **There is no save.** You can build and edit freely, but a person commits the view. Do not claim you saved anything, and do not try to reach a save through a trigger or an element event: automation actions have no submit or save type, and an element `submit` still needs a human click. 6. **Re-check `available()` after anything slow.** The API disarms the moment the user leaves the builder, and every command then fails with `apiClosed` while every read returns null. That is expected, not a fault. ### Result shape ```json { "ok": true, "version": 1, "dryRun": false, "applied": 3, "changed": ["contact", "email", "phone"], "errors": [], "warnings": ["[2] addElement: 'select' has no registered property 'placeholder'."] } ``` ### Error codes | Code | Meaning | Recovery | | --- | --- | --- | | `apiClosed` | The builder is not on screen | Ask the user to open it | | `unknownType` | Element type does not exist | Use the `hint`, or call `describeElementTypes()` | | `unknownElement` | No element with that name | Use the `hint`, or call `getTree()` | | `notAContainer` | The parent cannot hold children | Pick a container from the hint list | | `missingTab` | Tab style container needs a section | Pass `tab` | | `nameTaken` | Name already used | Pick another, or use `updateElement` | | `invalidName` | Name is not a valid identifier | Letters, digits, underscores, starting with a letter | | `cycle` | The target sits inside the element being moved | Pick a different parent | | `capabilityMissing` | The command needs a feature pack this builder does not have | Build without it, per the hint | | `unknownTemplate` | No template with that name | Call `describeTemplates()`, or create it with `upsertTemplate` | ## Layout is the part that goes wrong A view is rows, and each row holds one or more columns. **A new element gets its own row by default, so elements stack.** To place two fields side by side, name the existing `row`. ```js await api.execute([ { op: 'addElement', type: 'text', name: 'firstName', parent: 'contact', properties: { label: 'First name' } }, { op: 'addElement', type: 'text', name: 'lastName', parent: 'contact', row: 0, properties: { label: 'Last name' } }, ]); ``` Row indexes come from `getTree()` or from `addRow`. Target fields are `parent`, `page`, `tab`, `index`, `row`, `column`. See [Layout model](https://ngxviewbuilder.io/ai/layout-model) for how the same tree looks as raw JSON. **Do not set a percentage `width` on fields you want side by side.** Columns already share the row, and a fixed width fights it. Use `mobileWidth: '100%'` to make a pair stack on phones. For a whole section at once, `insertJson` takes the elements and the nested layout together, which is usually one command instead of five: ```js await api.execute({ op: 'insertJson', root: 'contact', elements: { contact: { type: 'panel', label: 'Contact' }, firstName: { type: 'text', label: 'First name', required: true }, lastName: { type: 'text', label: 'Last name', required: true }, }, rows: [{ columns: [{ elementRef: 'firstName' }, { elementRef: 'lastName' }] }], }); ``` ## What you can change Mutations: `addElement`, `insertJson`, `upsertTemplate`, `useTemplate`, `updateElement`, `deleteElement`, `moveElement`, `renameElement`, `duplicateElement`, `addRow`, `deleteRow`, `addPage`, `deletePage`, `renamePage`, `updatePage`, `setSettings`, `setHeader`, `upsertDataSource`, `deleteDataSource`, `upsertVariable`, `deleteVariable`, `upsertTrigger`, `deleteTrigger`, `upsertRule`, `deleteRule`, `upsertFragment`, `deleteFragment`, `setProcess`, `setLocalization`, `replaceStructure`. Actions: `switchTab`, `focusElement`, `setData`, `validate`, `setLanguage`, `setUiTranslations`, `setTheme`, `saveTemplate`, `deleteTemplate`, `saveSidebarGroup`, `deleteSidebarGroup`, `setTableSettings`, `setTableFilters`, `setRuntimeVariableContext`, `reloadDataSource`, `undo`, `redo`. Mutations are applied to a draft in order and committed together. Actions run afterwards, in order. Call `help()` for the parameters of each. ## Reuse before you invent Two habits save most of the rework. **Pick the element type that already does the job.** Call `describeElementTypes()` and choose from it. `customHtml` and hand written template markup carry no options, no validation and no events, so anything assembled that way has to be rebuilt later. The case that comes up most is a table column showing a status or a badge: that is a hosted element, `type: 'element'` with `elementType: 'badge'`, not markup. **Look in the template library before creating a template.** When `help().capabilities` includes `templates`, the view has one: ```js api.describeTemplates(); // { available, hosts, templates: [{ name, slots, hasCss, ... }] } ``` If the user names a card and that name is already in the library, reuse it. Create a missing one with `upsertTemplate` and bind it with `useTemplate`, which resolves the right reference property for the element type on its own: ```js await api.execute([ { op: 'upsertTemplate', name: 'person card', content: '
{{row[0]}} {{row[1]}}
' }, { op: 'useTemplate', name: 'peopleGrid', template: 'person card', fieldMap: { '0': 'fullName', '1': 'email' } }, ]); ``` Both need the `templates` capability and fail with `capabilityMissing` without it, because the reference properties are hidden then and the binding would render nothing. ## Checking your own work ```js await api.execute([ { op: 'switchTab', tab: 'preview' }, { op: 'setData', data: { age: 25 } }, { op: 'validate' }, ]); ``` `setData` pushes values through the same pipeline as typing, so `visibleIf`, `requireIf` and computed expressions re-evaluate. `validate` returns the issues. **Do not use `validate` to test whether a section is visible.** It runs on a fresh copy and does not apply container visibility to children, so a field inside a hidden panel is still reported as required. Read the preview instead. Note that a mutation rebuilds the view, which clears working data. Set data after your last structural change, not before. ## Your changes are one undo step Each `execute()` call is bracketed with a history checkpoint, so a whole batch collapses into a single undo for the user. `getAuditLog()` shows every call you made this session, which is worth reading back before you claim what you did. ## What gets checked Element types, property names, layout targets, name collisions and cycles are checked. So are the properties whose value is structured: `options` entries need a `value`, `validators` need a known `type` (and `custom` also needs a `condition`), `events` need a known action `type`, and nested elements in `columns` or `template` need a known type and a name. Everything else in an element body passes through untouched. That is why warnings matter: a misspelled property still lands and the runtime ignores it. ## Rules that still apply Everything in [JSON authoring rules](https://ngxviewbuilder.io/ai/json-authoring-rules), [Element rules](https://ngxviewbuilder.io/ai/element-rules) and [Properties reference](https://ngxviewbuilder.io/ai/properties-reference) applies unchanged. The command API changes how the JSON gets in, not what valid JSON looks like. --- ## AI reference Source: https://ngxviewbuilder.io/ai/ # AI reference This section is the **single indexed knowledge base for AI agents** that generate or modify NGX View Builder structure JSON. It is written for machine consumption first: an agent driving the builder over [MCP](https://ngxviewbuilder.io/developers/ai-command-api) loads slices of these pages into its prompts, and any other LLM setup (ChatGPT, Claude, a custom pipeline) can use the same pages as system-prompt material. Everything here has one goal: the agent must behave as a **strict NGX View Builder JSON author**, using real elements, documented properties and valid references, never as a generic frontend generator. ## Reading order An agent (or a person building prompts) should consume the pages in this order: 0. [Live builder command API](https://ngxviewbuilder.io/ai/command-api): check this first. If a builder is open and reachable, you edit it directly with JSON commands instead of handing back structure JSON. The authoring rules below still apply either way. 1. [Generation contract](https://ngxviewbuilder.io/ai/generation-contract): the core rules of engagement and output modes. 2. [Layout model](https://ngxviewbuilder.io/ai/layout-model): how `pages`, `rows`, `columns` and `elementRef` relate to the flat `elements` map. **Non-negotiable prerequisite for writing any structure JSON.** 3. [JSON authoring rules](https://ngxviewbuilder.io/ai/json-authoring-rules): the structure skeleton and hard rules. 4. [Element selection map](https://ngxviewbuilder.io/ai/element-selection-map): mapping user intent to the right element type. 5. [Logic & expression properties](https://ngxviewbuilder.io/ai/logic-and-expressions): `visibleIf`, `expression`, and friends. 6. [Element rules & value shapes](https://ngxviewbuilder.io/ai/element-rules): per-element expectations. 7. [Canonical properties reference](https://ngxviewbuilder.io/ai/properties-reference): the authoritative property list. 8. [Common mistakes](https://ngxviewbuilder.io/ai/common-mistakes): anti-patterns to avoid. 9. [Verified examples](https://ngxviewbuilder.io/ai/examples): complete, source-checked JSON for every element family, the full `table` feature set, dynamic tables and panels, data sources, variables and expressions. 10. [Legacy form migration](https://ngxviewbuilder.io/ai/legacy-form-migration): only when converting forms from a legacy form-builder JSON format. ## Index | Page | What it answers | Load when | | --- | --- | --- | | [Live builder command API](https://ngxviewbuilder.io/ai/command-api) | Driving an open builder directly: detection, bootstrap sequence, the `execute` contract, error recovery, row targeting | First, whenever the `nvb_*` MCP tools are available | | [Generation contract](https://ngxviewbuilder.io/ai/generation-contract) | How the agent must behave; output modes; prompt templates | Always | | [Layout model](https://ngxviewbuilder.io/ai/layout-model) | The layout tree: `pages` → `rows` → `columns` → `elementRef`, container nesting, `tabRows`, where widths live, full worked example | Always, before any JSON is written | | [JSON authoring rules](https://ngxviewbuilder.io/ai/json-authoring-rules) | Skeleton, `pages`/`elements` rules, naming, layout, value shapes | Always | | [Canonical properties reference](https://ngxviewbuilder.io/ai/properties-reference) | Every supported property per element type, settings, data sources | Always | | [Element rules & value shapes](https://ngxviewbuilder.io/ai/element-rules) | Per-element usage rules and value shapes | Always | | [Common mistakes](https://ngxviewbuilder.io/ai/common-mistakes) | Known anti-patterns with corrections | Always; especially in review mode | | [Verified examples](https://ngxviewbuilder.io/ai/examples) | Complete working JSON: layout, all element families, `table` end to end, `dynamicTable`, `dynamicPanel`, data sources, variables, expressions, actions | When building anything non-trivial; always for `table` | | [Element selection map](https://ngxviewbuilder.io/ai/element-selection-map) | Which element type fits the user's intent | When element choice is ambiguous | | [Logic & expression properties](https://ngxviewbuilder.io/ai/logic-and-expressions) | Expression fields, syntax rules, correct/incorrect examples | When the request involves logic | | [Legacy form migration](https://ngxviewbuilder.io/ai/legacy-form-migration) | Element/property/expression mapping from a legacy form-builder JSON format | When converting legacy form JSON | | [API service reference](https://ngxviewbuilder.io/developers/api-service), [Events reference](https://ngxviewbuilder.io/developers/events) | What every host API method / event does, its parameters, payloads and return values | When the user asks a development or integration question | Every JSON block on those pages is machine-checked against the library source by `npm run validate:ai-json` in this repo: element types against the element registry, property names against the builder property datasets, sub-objects against their TypeScript interfaces. If a property appears in these docs, it exists. ## Using this with ChatGPT, Claude or Gemini Three single-file bundles are published, in increasing size: | File | Contains | Use it when | | --- | --- | --- | | [`/llms.txt`](https://ngxviewbuilder.io/llms.txt) | a linked index, a few KB | the model can fetch URLs itself | | [`/llms-authoring.txt`](https://ngxviewbuilder.io/llms-authoring.txt) | **every page in this AI section**, roughly 55k tokens | the task is writing or fixing structure JSON. This is the one to paste into a chat | | [`/llms-full.txt`](https://ngxviewbuilder.io/llms-full.txt) | the whole site, roughly 135k tokens | the question also covers embedding, the host API, events, theming or plugins | `llms-authoring.txt` is deliberately the smaller bundle: it drops the creator guides, pricing and host-integration pages, which are noise when the only deliverable is JSON. Paste it once at the start of a session, then describe the view you want. A caveat worth stating plainly: a pasted file is reference material, not a guarantee. Models still skim long context. The two habits that matter most are asking for the layout tree to be stated in words before the JSON, and running the result back through the [pre-return checklist](https://ngxviewbuilder.io/ai/json-authoring-rules#pre-return-checklist). The agent does more than write JSON. It also **consults developers**: what an API method means, what it returns, how to wire an event, how to embed the builder or runtime. For those answers the retrieval map has a dedicated `developer` source group pointing at the developer documentation. The agent also draws on the human documentation ([element pages](https://ngxviewbuilder.io/creators/elements/), [expressions](https://ngxviewbuilder.io/creators/expressions), [conditional logic](https://ngxviewbuilder.io/creators/conditional-logic), [validation](https://ngxviewbuilder.io/creators/validation), [events & actions](https://ngxviewbuilder.io/creators/events-actions), [data sources](https://ngxviewbuilder.io/creators/data-sources) and [variables](https://ngxviewbuilder.io/creators/variables)) and on library source files: interfaces, enums and property datasets, which are always the final source of truth. ## The machine index: `retrieval-map.json` [`retrieval-map.json`](https://ngxviewbuilder.io/ai/retrieval-map.json) (next to this page) is the machine-readable index. It maps request profiles and element types to the exact files the agent should load, with per-file character budgets: - **`roots`**: where the docs and the library live, relative to the workspace root (the folder that contains all `ngx-view-builder*` projects). - **`universal`**: sources injected into every prompt. - **`types`**: per element type, which docs page, interface, and property dataset to load. - **`selection`, `review`, `legacyForms`, `logic`, `validators`, `events`, `actions`, `data`**: sources added when the request profile matches. - **`groupFallback`**: group-level docs used when no concrete element type was resolved. Paths use two prefixes: `docs:` (resolved against `roots.docs`, i.e. this documentation) and `lib:` (resolved against `roots.library`, i.e. the library source). An agent loads this file at startup, so when documentation moves, updating this map is the only change needed. --- ## AI: Generation contract Source: https://ngxviewbuilder.io/ai/generation-contract # AI: Generation contract This page defines the rules of engagement for `ChatGPT`, `Claude`, and other agents that generate or fix NGX View Builder structures, or consult developers about the library. The core idea is simple: when producing structures, the agent must operate not as a generic frontend generator, but as a strict **NGX View Builder JSON** author. ## Core contract - Generate only NGX View Builder JSON if the user explicitly asked to generate or modify a form or view. - Do not generate custom `Angular`, `HTML`, `CSS`, `TypeScript`, an API layer, or non-existent NGX View Builder properties. - Use only real built-in elements and documented fields. - If the user asks for a review, a comment, or suggestions, you may respond in prose, but do not change the JSON without an explicit request. - If the user asks to extend an existing form, the priority is to preserve the existing structure and change only what was requested. - If the user asks a **development or integration question** (host API, events, embedding, theming, data sources), answer in prose with concrete code examples based on the developer documentation. Do not invent methods or events that are not documented. ## What the agent should read first 1. [Layout model](https://ngxviewbuilder.io/ai/layout-model) 2. [JSON authoring rules](https://ngxviewbuilder.io/ai/json-authoring-rules) 3. [Element selection map](https://ngxviewbuilder.io/ai/element-selection-map) 4. [Logic and expression properties](https://ngxviewbuilder.io/ai/logic-and-expressions) 5. [Element rules and value shapes](https://ngxviewbuilder.io/ai/element-rules) 6. [Common mistakes](https://ngxviewbuilder.io/ai/common-mistakes) After that, consult the general reference pages: - [Structure JSON overview](https://ngxviewbuilder.io/developers/structure-json) - [Layout & containers](https://ngxviewbuilder.io/creators/layout) - [Element catalog](https://ngxviewbuilder.io/creators/elements/) - [Expressions](https://ngxviewbuilder.io/creators/expressions) - [Canonical properties reference](https://ngxviewbuilder.io/ai/properties-reference) For developer-consulting answers, the sources of truth are: - [API service reference](https://ngxviewbuilder.io/developers/api-service): every host API method, its parameters and return values - [Events reference](https://ngxviewbuilder.io/developers/events): every event the builder and runtime emit, with payloads - [Embedding the builder](https://ngxviewbuilder.io/developers/builder-integration) and [Rendering views](https://ngxviewbuilder.io/developers/runtime-integration) - [Data source integration](https://ngxviewbuilder.io/developers/data-sources) and [Runtime variables](https://ngxviewbuilder.io/developers/runtime-variables) ## Mandatory rules - `pages` describes layout. `elements` describes configuration. Neither ever does the other's job. - `elements` is a **flat** map where the key matches `element.name`. It never nests. - `pages[*].rows[*].columns[*].elementRef` is a string that must point to an existing `elements` entry. - Every `page` must have a corresponding `elements[pageName]` entry with `type: "page"`. - **A container element never contains its children.** The children of a `panel`, `dynamicPanel`, `dialog` or `emptyBlock` are attached to the column that references it, in `column.rows`; for `tabs`, `tabsPro`, `accordion`, `splitter` and `progressFlow`, in `column.tabRows`. Writing `rows` (or `children`, `items`, `fields`) inside an element definition produces an empty container and orphaned fields, with no error anywhere. - A row object has only `columns`. A column object has only `elementRef` plus `rows` / `tabRows`. Widths and other properties on a column are silently dropped; they belong on the element. - Columns with no width split their row evenly. Do not add `"width": "50%"` to get halves. - If there is interdependency logic between fields, use `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, `resetIf`, `expression`, and, when needed, `logicExecutionMode: "onChange"`. - If there is a datasource scenario, use documented `dataSources` and real `dataSourceName` references. - Do not use self-reference expressions, e.g. `"{el2} == 'x' ? 'y' : {el2}"`. - Do not return explanatory text around the JSON if the user asked for a structure. ## Recommended system prompt template ```text You are a strict NGX View Builder JSON generator and integration consultant. Work only with the NGX View Builder documented JSON model and documented host API. Do not use custom Angular, custom components, or invented properties. If the user asks to generate or modify a form, return only valid JSON. If the user asks for a review, comment, suggestions, or an integration/API question, respond in prose with documented examples. The structure has two independent halves: - "pages" is a layout tree: pages -> rows -> columns, each column carrying an "elementRef" STRING. - "elements" is a FLAT map of definitions, keyed by element name. It never nests. Container elements (page, panel, dynamicPanel, dialog, emptyBlock, tabs, tabsPro, accordion, splitter, progressFlow) do NOT contain their children. The children are attached to the COLUMN that references the container, in column.rows (or column.tabRows for the tab-like ones). Writing "rows"/"children"/"items" inside an element definition renders an empty container and drops the fields silently. A row object has only "columns". A column object has only "elementRef" plus "rows"/"tabRows". Widths (width, tabletWidth, mobileWidth, fitContent) go on the ELEMENT, and only when the split must be uneven: columns with no width already split their row evenly. A page is a step or screen. Titled sections on one screen are panel elements inside a single page, never separate pages. Before generating: 1. Decide which NGX View Builder elements best match the task. 2. List the visual rows top to bottom and count the fields per row before writing JSON. 3. Verify the required value shape and mandatory properties. 4. Verify that logic is not self-referential and that all elementRefs point to existing elements. 5. Verify that no element definition contains a rows/children/items array and that no column carries anything besides elementRef/rows/tabRows. 6. If extending an existing form, do not modify unrelated parts. ``` ## Recommended user prompt format Instead of a vague "create a form", give the agent these signals: - goal: what the form should allow the user to do - data: which fields are needed - logic: which fields hide, compute, fill, or validate others - data sources: what is loaded from an API or another datasource - update mode: new form or extension of an existing one Example: ```text Extend an existing NGX View Builder form. Required: - if first name is John, last name is automatically set to Doe - date of birth cannot be in the future - add a dynamicTable for family members with add row and delete row - use only NGX View Builder JSON, no comments ``` ## What to do if the user's request is ambiguous - If a term is ambiguous, decide based on the [Element selection map](https://ngxviewbuilder.io/ai/element-selection-map). - If the user says "dynamic table", it is almost always `dynamicTable`, not `table`. - If the user says "server-side table", "filtering", "paging", "row actions", it is almost always `table`. - If the user wants a review or architectural assessment, do not generate new JSON without a request. ## Output modes ### 1. Generation mode - Return only JSON. - No markdown. - No explanation before or after the JSON. ### 2. Review mode - You may provide a brief analysis. - If you suggest fixes, clearly separate comments from the proposed JSON. ### 3. Extension mode - Preserve the entire existing form. - Change only what is needed. - Do not remove `settings`, `pages`, `elements`, `localization`, or `dataSources` just because they were not mentioned in the latest prompt. ### 4. Consulting mode - The user asks how something works: a host API method, an event, embedding, theming, a data source, an expression function. - Answer in prose with short, correct code examples (TypeScript for host API, JSON for structures). - State what a method returns and when an event fires, exactly as documented. If the documentation does not describe it, say so instead of guessing. - Do not generate a whole structure unless asked. --- ## AI: Layout model (pages, rows, columns, elementRef) Source: https://ngxviewbuilder.io/ai/layout-model # AI: Layout model (pages, rows, columns, elementRef) This is the page that stops the single most damaging class of generated-JSON errors: getting the layout tree wrong. Read it before writing any structure JSON. If anything on this page contradicts your intuition about form builders, this page wins. ## The one rule that explains everything NGX View Builder splits a view into **two independent halves**: | Half | What it is | What it contains | |---|---|---| | `pages` | The **layout tree**. Position only. | Nested `rows` and `columns`. Each column carries one `elementRef` string. | | `elements` | A **flat dictionary** of element definitions. Configuration only. | One entry per element, keyed by its `name`. No nesting, no children, no position. | `elementRef` is the only bridge between them. It is a **string key into `elements`**, nothing else. ``` pages[0] elements └─ rows[0] ┌──────────────────────────────────┐ └─ columns[0] │ "firstName": { type: "text", ... }│ └─ elementRef: "firstName" ────►│ │ │ "lastName": { type: "text", ... }│ └─ columns[1] │ │ └─ elementRef: "lastName" ────►│ "page1": { type: "page", ... } │ └──────────────────────────────────┘ ``` Two consequences that agents get wrong constantly: 1. **Structure lives only in `pages`.** An element definition never contains its children. 2. **Configuration lives only in `elements`.** A column never contains element properties. ## Exact object shapes These are the real TypeScript interfaces (`core/shared/interfaces/structure.interface.ts`). Keys not listed here are ignored by the renderer. ```typescript interface IStructure { schemaVersion?: number; settings: ISettings; header?: IHeader; pages: IPage[]; elements: { [name: string]: IBaseElement }; dataSources?: IDataSource[]; localization?: ILocalization; } interface IPage { name: string; // must match an elements[name] entry with type "page" rows: IRow[]; status?: 'default' | 'readonly'; disabled?: boolean; readOnly?: boolean; } interface IRow { columns: IColumn[]; // this is the ONLY key on a row } interface IColumn { elementRef: string; // required, points into elements rows?: IRow[]; // children of a container element tabRows?: Record; // children of tabs/accordion/splitter/progressFlow fragmentRef?: string; // platform fragments only fragmentBindings?: Record; fragmentMode?: 'append' | 'replace'; } ``` **A row object has exactly one key: `columns`.** **A column object has `elementRef` plus, when the element is a container, `rows` or `tabRows`. Nothing else.** Anything you are tempted to write on a column (`width`, `mobileWidth`, `label`, `type`, `span`, `size`, `flex`, `colSpan`) does not exist there. It is silently dropped. ## How a row renders - A `row` is a **horizontal band**. Its `columns` sit side by side, left to right. - Vertical order comes from the order of `rows` in the array. There is no `order` property. - **A column with no width takes an equal share of the row.** The renderer applies `flex: 1 1 0` when the element has no `width` (`core/shared/utils/responsive-width.ts`). So a 50/50 pair is just two columns in one row, with no widths at all: ```json { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] } ``` Three equal fields: three columns. One full-width field: one column. **Do not write `"width": "50%"` to get halves.** Equal split is the default, and writing widths on columns does nothing anyway. ## Where widths belong Width is a property of the **element**, in the `elements` map: | Property | Meaning | |---|---| | `width` | Desktop width, e.g. `"100%"`, `"320px"`, `"calc(50% - 8px)"` | | `tabletWidth` | Overrides `width` on tablet | | `mobileWidth` | Overrides `width` and `tabletWidth` on mobile | | `fitContent` | Shrink to content instead of filling | A bare number is treated as pixels. A percentage, `calc()`, `min()`, `max()` or `clamp()` is treated as relative and pinned exactly; a fixed size is allowed to shrink (`min(width, 100%)`). Only set these when you need something **other** than an equal split, for example a narrow field beside a wide one: ```json { "postCode": { "name": "postCode", "label": "Post code", "type": "text", "width": "160px" }, "street": { "name": "street", "label": "Street", "type": "text" } } ``` Mobile stacking happens by giving the element `"mobileWidth": "100%"`, again on the element, never on the column. ## Nesting: the mistake that breaks everything Container elements do **not** hold their children. The **column that references the container** holds them, in its own `rows` array. ### Wrong (children inside the element definition) ```json { "pages": [ { "name": "page1", "rows": [{ "columns": [{ "elementRef": "panelGeneral" }] }] } ], "elements": { "panelGeneral": { "name": "panelGeneral", "type": "panel", "label": "General", "rows": [ { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] } ] } } } ``` The panel renders **empty**. `IBaseElement` has no `rows`, so the array is ignored, and `firstName` / `lastName` are never placed anywhere. ### Right (children inside the column) ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "panelGeneral", "rows": [ { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "type": "page", "label": "Page 1" }, "panelGeneral": { "name": "panelGeneral", "type": "panel", "label": "General" }, "firstName": { "name": "firstName", "type": "text", "label": "First name" }, "lastName": { "name": "lastName", "type": "text", "label": "Last name" } } } ``` Nesting is recursive: a nested column may itself carry `rows`, to any depth. Note that `parentName` exists on `IBaseElement` but is **not** how you build the tree. Never use it to declare parentage; the layout tree is the only source of structure. ## Which element types accept children Exactly these ten types are containers (`runtime-preview.ts`, `isContainerElement`): | Type | Children go in | Notes | |---|---|---| | `page` | `page.rows` | The page itself; its rows are the top level | | `panel` | `column.rows` | The normal grouping box, with a title from `label` | | `dynamicPanel` | `column.rows` | Repeatable group; value is an array of objects | | `dialog` | `column.rows` | An in-view modal element | | `emptyBlock` | `column.rows` | Unstyled layout box with flex/grid controls | | `tabs` | `column.tabRows` | Keyed by tab value | | `tabsPro` | `column.tabRows` | Keyed by item value | | `accordion` | `column.tabRows` | Keyed by item value | | `splitter` | `column.tabRows` | Keyed by panel value | | `progressFlow` | `column.tabRows` | Keyed by step value | **Every other type is a leaf.** `text`, `select`, `table`, `dynamicTable`, `messageCard`, `statsCard`, `richText` and the rest never receive `rows`. The two tables are the exception that proves the rule: their cells are declared on the element, not in the layout tree, and the two do not use the same property. `table` uses `columnsConfig`, and each entry is a column keyed by `key`. `dynamicTable` uses `columns`, and each entry is a full element definition keyed by `name`, because every cell is an editable control. Neither uses `column.rows`. ### `tabRows` keys For tab-like containers, `tabRows` is an object keyed by the **`value` of each declared tab / item / panel / step** (falling back to `id`, then `tab1`, `tab2`, ...). The keys must match the container's own list property: | Type | List property | Key source | |---|---|---| | `tabs` | `tabs` | `tabs[i].value` | | `tabsPro` | `items` | `items[i].value` | | `accordion` | `items` | `items[i].value` | | `splitter` | `panels` | `panels[i].value` | | `progressFlow` | `items` | `items[i].value` | ```json { "columns": [ { "elementRef": "personTabs", "tabRows": { "general": [{ "columns": [{ "elementRef": "firstName" }] }], "contact": [{ "columns": [{ "elementRef": "email" }] }] } } ] } ``` with ```json { "personTabs": { "name": "personTabs", "type": "tabs", "label": "Person", "tabs": [ { "value": "general", "label": "General" }, { "value": "contact", "label": "Contact" } ] } } ``` ## Pages are not sections A `page` is a **step or screen**, not a visual section. Multiple entries in `pages` produce a multi-step view, navigated by the built-in pager or stepper (`settings.pageNavigationMode`). To draw titled sections stacked on one screen, use **one page containing several `panel` elements**. Do not create one page per section. Every page needs its twin in `elements`: ```json { "pages": [{ "name": "personPage", "rows": [] }], "elements": { "personPage": { "name": "personPage", "label": "Person", "type": "page", "hideHeader": true } } } ``` Page element properties: `name`, `label`, `description`, `type`, `visibleIf`, `disableIf`, `readonlyIf`, `hideHeader`, `removeBackgraund` (spelled exactly like that), `pageBackgroundColor`, `pagePadding`, `mobilePadding`. Nothing else. ## Building a layout from a screenshot or description Follow this order every time. Do not start writing JSON at step 1. 1. **Read the visual top to bottom.** List every field, in order, with its label. 2. **Find the section headings.** Each heading becomes one `panel` element whose `label` is that heading. 3. **Slice each section into horizontal bands.** Fields on the same visual line belong to the same `row`. A field on its own line is its own `row` with one column. 4. **Count the columns per band.** Two fields side by side is two columns. Three is three. Equal split is free; only reach for widths when the visual is clearly uneven. 5. **Write `elements` first.** Every panel, every field, the page. Flat, keyed by `name`, each with `name`, `label`, `type`. 6. **Write `pages` second.** Panels at the top level; each panel's column carries the section's `rows`. 7. **Verify** with the checklist below. Counting rows and columns before touching JSON is what keeps the output faithful to the picture. ## Verification checklist Run all of these mentally before returning JSON. 1. Every `elementRef` string resolves to a key in `elements`. 2. Every key in `elements` appears exactly once as some `elementRef` (or is a `page`). 3. Every entry of `pages` has a matching `elements[name]` with `type: "page"`. 4. No object in `elements` has a `rows`, `columns` or `children` key. The two exceptions are `table.columnsConfig` and `dynamicTable.columns`. 5. No column object has any key other than `elementRef`, `rows`, `tabRows`, `fragmentRef`, `fragmentBindings`, `fragmentMode`. 6. No row object has any key other than `columns`. 7. `rows` on a column appears only when the referenced element is one of the ten container types. 8. `tabRows` keys match the container's declared tab/item/panel/step values. 9. Widths, if present at all, are on elements, not columns. 10. `name` values are unique across the whole structure. ## Full worked example A dialog with four titled sections, mixed one-, two- and three-field rows, conditional document fields, and a footer button pair. Every property used below exists in [the properties reference](https://ngxviewbuilder.io/ai/properties-reference). ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "lt-LT", "width": "640px", "widthUnit": "px", "renderMode": "dialog", "dialogTitle": "Add a new person", "dialogWidth": "640px", "dialogShowCloseButton": true, "showSubmitButton": false }, "pages": [ { "name": "personPage", "rows": [ { "columns": [ { "elementRef": "panelGeneral", "rows": [ { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] }, { "columns": [{ "elementRef": "personalCode" }, { "elementRef": "birthDate" }] }, { "columns": [{ "elementRef": "citizenship" }, { "elementRef": "gender" }] }, { "columns": [{ "elementRef": "isStudent" }] } ] } ] }, { "columns": [ { "elementRef": "panelContact", "rows": [ { "columns": [{ "elementRef": "email" }] }, { "columns": [{ "elementRef": "phone" }] }, { "columns": [{ "elementRef": "declaredAddress" }] }, { "columns": [{ "elementRef": "country" }] }, { "columns": [{ "elementRef": "locality" }, { "elementRef": "street" }] }, { "columns": [ { "elementRef": "houseNumber" }, { "elementRef": "flatNumber" }, { "elementRef": "postCode" } ] } ] } ] }, { "columns": [ { "elementRef": "panelBank", "rows": [ { "columns": [{ "elementRef": "bankAccount" }] }, { "columns": [{ "elementRef": "bankName" }] }, { "columns": [{ "elementRef": "bankSwift" }] }, { "columns": [{ "elementRef": "bankAddress" }] } ] } ] }, { "columns": [ { "elementRef": "panelDocument", "rows": [ { "columns": [{ "elementRef": "hasDocument" }] }, { "columns": [{ "elementRef": "documentType" }] }, { "columns": [ { "elementRef": "documentNumber" }, { "elementRef": "documentValidUntil" } ] } ] } ] }, { "columns": [ { "elementRef": "panelActions", "rows": [ { "columns": [{ "elementRef": "cancelButton" }, { "elementRef": "saveButton" }] } ] } ] } ] } ], "elements": { "personPage": { "name": "personPage", "label": "Add a new person", "type": "page", "hideHeader": true }, "panelGeneral": { "name": "panelGeneral", "label": "Bendri asmens duomenys", "type": "panel", "showBorder": false, "panelPadding": "0px", "titleUnderline": true }, "firstName": { "name": "firstName", "label": "Vardas", "type": "text", "required": true, "requiredMessage": "Enter the first name" }, "lastName": { "name": "lastName", "label": "Last name", "type": "text", "required": true, "requiredMessage": "Enter the last name" }, "personalCode": { "name": "personalCode", "label": "Asmens kodas", "type": "text", "maskType": "digits", "maxlength": 11, "required": true, "requiredMessage": "Enter the personal code" }, "birthDate": { "name": "birthDate", "label": "Gimimo data", "type": "datepicker", "placeholder": "Pasirinkti", "pickerMode": "date", "format": "YYYY-MM-DD", "required": true }, "citizenship": { "name": "citizenship", "label": "Citizenship", "type": "select", "showSearch": true, "strictOptions": true, "required": true, "options": [ { "value": "LT", "label": "Lietuva" }, { "value": "LV", "label": "Latvija" }, { "value": "EE", "label": "Estija" } ] }, "gender": { "name": "gender", "label": "Lytis", "type": "select", "strictOptions": true, "required": true, "options": [ { "value": "M", "label": "Vyras" }, { "value": "F", "label": "Moteris" } ] }, "isStudent": { "name": "isStudent", "label": "", "type": "singleCheckbox", "checkboxLabel": "Person qualifies as a student", "defaultValue": false }, "panelContact": { "name": "panelContact", "label": "Kontaktiniai duomenys", "type": "panel", "showBorder": false, "panelPadding": "0px", "titleUnderline": true }, "email": { "name": "email", "label": "Email", "type": "text", "inputMode": "email", "required": true, "validators": [ { "type": "email", "message": "Invalid email format" } ] }, "phone": { "name": "phone", "label": "Tel. Nr.", "type": "phoneInput", "defaultCountryCode": "LT" }, "declaredAddress": { "name": "declaredAddress", "label": "Deklaruotos gyvenamosios vietos adresas", "type": "text", "readOnly": true, "expression": "{street} + ' ' + {houseNumber} + ', ' + {locality}", "logicExecutionMode": "onChange" }, "country": { "name": "country", "label": "Country", "type": "select", "showSearch": true, "strictOptions": true, "options": [ { "value": "LT", "label": "Lietuva" }, { "value": "LV", "label": "Latvija" }, { "value": "EE", "label": "Estija" } ] }, "locality": { "name": "locality", "label": "Locality", "type": "text" }, "street": { "name": "street", "label": "Street", "type": "text" }, "houseNumber": { "name": "houseNumber", "label": "Namo numeris", "type": "text", "mobileWidth": "100%" }, "flatNumber": { "name": "flatNumber", "label": "Buto numeris", "type": "text", "mobileWidth": "100%" }, "postCode": { "name": "postCode", "label": "Post code", "type": "text", "mobileWidth": "100%" }, "panelBank": { "name": "panelBank", "label": "Bank account details", "type": "panel", "showBorder": false, "panelPadding": "0px", "titleUnderline": true }, "bankAccount": { "name": "bankAccount", "label": "Bank account number", "type": "text" }, "bankName": { "name": "bankName", "label": "Banko pavadinimas", "type": "text" }, "bankSwift": { "name": "bankSwift", "label": "Banko SWIFT (BIC) kodas", "type": "text", "minlength": 8, "maxlength": 11 }, "bankAddress": { "name": "bankAddress", "label": "Banko adresas", "type": "text" }, "panelDocument": { "name": "panelDocument", "label": "Asmens dokumento duomenys", "type": "panel", "showBorder": false, "panelPadding": "0px", "titleUnderline": true }, "hasDocument": { "name": "hasDocument", "label": "Dokumentas", "type": "radio", "showInline": true, "defaultValue": "Y", "required": true, "options": [ { "value": "Y", "label": "Taip" }, { "value": "N", "label": "Ne" } ] }, "documentType": { "name": "documentType", "label": "Dokumento tipas", "type": "select", "strictOptions": true, "visibleIf": "{hasDocument} == 'Y'", "requireIf": "{hasDocument} == 'Y'", "logicExecutionMode": "onChange", "options": [ { "value": "PASSPORT", "label": "Pasas" }, { "value": "ID_CARD", "label": "ID card" } ] }, "documentNumber": { "name": "documentNumber", "label": "Dokumento numeris", "type": "text", "visibleIf": "{hasDocument} == 'Y'", "requireIf": "{hasDocument} == 'Y'", "logicExecutionMode": "onChange" }, "documentValidUntil": { "name": "documentValidUntil", "label": "Dok. galioja iki", "type": "datepicker", "placeholder": "Pasirinkti", "pickerMode": "date", "format": "YYYY-MM-DD", "visibleIf": "{hasDocument} == 'Y'", "logicExecutionMode": "onChange" }, "panelActions": { "name": "panelActions", "label": "", "type": "panel", "showBorder": false, "panelPadding": "0px", "contentJustify": "flex-end", "contentGap": "8px" }, "cancelButton": { "name": "cancelButton", "label": "", "type": "button", "text": "Cancel", "variant": "outline", "tone": "neutral", "fitContent": true }, "saveButton": { "name": "saveButton", "label": "", "type": "button", "text": "Save", "variant": "solid", "tone": "primary", "fitContent": true, "events": [{ "trigger": "click", "type": "submit", "validateForm": true }] } }, "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` Read that example structurally, not as a template to copy verbatim. The shape is what matters: four panels at the top level of `pages`, each panel's column carrying the section's rows, and every field defined once, flat, in `elements`. ## Quick reference card ``` pages[] → { name, rows[] } rows[] → { columns[] } (only key: columns) columns[] → { elementRef, rows?, tabRows? } (no widths, no props) rows[] → recursion, containers only elements{} → flat map, key === element.name every element → { name, label, type, ...properties } containers → hold NO children widths → width / tabletWidth / mobileWidth / fitContent, here only ``` Related: [JSON authoring rules](https://ngxviewbuilder.io/ai/json-authoring-rules), [Common mistakes](https://ngxviewbuilder.io/ai/common-mistakes), [Canonical properties reference](https://ngxviewbuilder.io/ai/properties-reference). --- ## AI: JSON authoring rules Source: https://ngxviewbuilder.io/ai/json-authoring-rules # AI: JSON authoring rules This page defines how an agent must construct NGX View Builder JSON. ::: warning Read the layout model first A structure has two independent halves: `pages` is a **layout tree of positions**, `elements` is a **flat dictionary of definitions**, and `elementRef` is the only bridge between them. Containers do not hold their children; the column that references a container holds them in its own `rows`. Widths belong to elements, never to columns. The complete rules, with the exact object shapes and a full worked example, are in [Layout model](https://ngxviewbuilder.io/ai/layout-model). Do not write layout JSON before reading it. ::: ## Minimal skeleton ```json { "settings": { "language": "en", "locale": "en-US", "renderMode": "page" }, "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "field1" } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page" }, "field1": { "name": "field1", "label": "Field 1", "type": "text" } }, "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` ## Element type name rules **Type strings are case-sensitive. Copy them exactly.** Never invent casing variants. | Correct | Wrong (do not use) | |---|---| | `"datepicker"` | `"datePicker"`, `"DatePicker"`, `"date-picker"` | | `"singleCheckbox"` | `"SingleCheckbox"`, `"single_checkbox"` | | `"dynamicTable"` | `"DynamicTable"`, `"dynamic-table"` | | `"dynamicPanel"` | `"DynamicPanel"` | | `"multiSelect"` | `"MultiSelect"`, `"multi-select"` | | `"toggleSwitch"` | `"ToggleSwitch"`, `"toggle-switch"` | | `"richText"` | `"richtext"`, `"RichText"` | | `"fileUpload"` | `"fileupload"`, `"FileUpload"` | | `"phoneInput"` | `"PhoneInput"`, `"phone-input"` | | `"listGrid"` | `"ListGrid"`, `"list-grid"` | The canonical full list is in [Common mistakes, entry 14](https://ngxviewbuilder.io/ai/common-mistakes#_14-wrong-element-type-casing). ## Structure rules ### `settings` - Keep only settings that are actually used. - If the user does not request a complex modal/dialog mode, `language`, `locale`, and `renderMode` are sufficient. - Do not invent unnecessary `dialog*`, `stepper*`, or `pageNavigation*` properties if they are not in use. ### `pages` - `pages` describes layout only. - Every page must have a `name` and a `rows` array. Those are its only structural keys. - A row object has exactly one key: `columns`. - A column object has `elementRef`, plus `rows` or `tabRows` when it references a container. Nothing else. `width`, `mobileWidth`, `span`, `label`, `type` on a column are silently dropped. - `rows[*].columns[*].elementRef` is a **string key** into the `elements` map, not an object. - Do not embed full element objects inside `pages`. - A `page` is a step or screen. Titled sections on one screen are `panel` elements inside a single page, not separate pages. ### `elements` - `elements` is an object, not an array, and it is **flat**. It never nests. - The key must match `element.name`. - Every element must have at least `name`, `label`, and `type`. - If `pages[*].name = "pageCustomer"`, there must be an `elements.pageCustomer` entry with `type: "page"`. - **No element definition lists its children.** `rows`, `columns`, `children`, `items`, `fields` on a `panel` / `dynamicPanel` / `tabs` / `dialog` do not exist and are ignored. The exception is the two tables, whose cells are not layout: `table` declares them in `columnsConfig` keyed by `key`, and `dynamicTable` declares them in `columns` keyed by `name`. ### `localization` - For a single-language form, the minimum is: - `defaultLanguage` - `languages` - For multi-language forms, a `texts` section may also be present. ### `dataSources` - Create only when the user genuinely needs external data or actions. - `dataSourceName` references in elements must point to a real datasource. - If the form has no integration scenarios, it is better to omit `dataSources`. ## Name and reference rules - All `name` values must be unique throughout the structure. - `elementRef` must point to an existing `elements` entry. - If an element is a container with inner fields, those child elements must also have unique `name` values. - Do not reuse the same `name` for multiple different elements. ## What the agent must do when extending an existing form - Preserve existing `settings`, `pages`, `elements`, `localization`, and `dataSources` unless the user explicitly asks to rebuild everything from scratch. - Modify only the related sections. - When adding a new element, you must: - add it to `elements` - insert the `elementRef` in the appropriate `page/row/column` - When adding logic between fields, verify that both fields already exist. ## Layout rules - A single `row` is a horizontal band; its `columns` sit side by side. Vertical order is the order of `rows`. - **A column with no width takes an equal share of the row.** For a two-column layout, one `row` with two `columns` and no widths is sufficient. Never write `"width": "50%"` to get halves. - Widths (`width`, `tabletWidth`, `mobileWidth`, `fitContent`) are **element** properties, set in the `elements` map, and only when the split must be uneven. - Exactly ten types accept children: `page`, `panel`, `dynamicPanel`, `dialog`, `emptyBlock` (via `column.rows`) and `tabs`, `tabsPro`, `accordion`, `splitter`, `progressFlow` (via `column.tabRows`, keyed by each tab/item/panel/step `value`). Every other type is a leaf. - Container inner layouts must remain in the NGX View Builder model, not via custom HTML. - `parentName` is not how parentage is declared. The layout tree is. Worked examples and the exact interfaces: [Layout model](https://ngxviewbuilder.io/ai/layout-model). ## Value shape rules - `text`, `textarea`, `richText` typically store a `string`. - `number`, `slider` store a `number` or `string` depending on `valueStorageType`. - `singleCheckbox`, `toggleSwitch`, `toggleButton` typically store a `boolean`. - `checkbox`, `multiSelect` typically store an array. - `select`, `radio`, `autocomplete` typically store a single value. - `dateRange` must return an object with `dateFrom` and `dateTo`. - `dynamicPanel` and `dynamicTable` typically store an array of objects. - `numberStepper` stores a `number`; `timePicker` stores a time string. - `signaturePad` stores signature image data keyed under the element's `name`. - `listBox`, `selectButton` store a single value, or an array when multi-select/`multiple` is enabled. - `progressBar` stores a `number`. ## Logic rules - Use `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, `resetIf` for boolean conditions. - Use `expression` to compute and write a value. - When logic depends on changes in another field, typically add `logicExecutionMode: "onChange"`. - Do not use self-reference `expression`. ## Validator field rules Validators are separate sub-objects in the `validators` array. They have their own field names that differ from element-level logic fields: | Element-level (correct placement) | Validator-level (correct placement) | |---|---| | `element.visibleIf` | `validator.condition` | | `element.requireIf` | `validator.applyIf` | | `element.expression` | _(does not exist on validators)_ | Never put `visibleIf`, `requireIf`, `readonlyIf`, `disableIf`, `expression`, or `resetIf` inside a validator object. Never use the field name `expression` on a validator; the correct field is `condition`. **Polarity:** `condition` is the *failing* check. The validator error is shown while `condition` evaluates to `true`. Use `applyIf` to run a validator only in certain cases (it runs while `applyIf` is truthy). Correct: ```json { "name": "age", "type": "number", "visibleIf": "{skipAge} != true", "validators": [ { "type": "min", "value": 0, "message": "Cannot be negative" }, { "type": "max", "value": 120, "message": "Invalid age", "applyIf": "{country} == 'LT'" } ] } ``` ## What must not be generated - Custom `Angular` components. - Non-existent NGX View Builder properties. - Full element objects embedded in `pages`. - Pseudo-code instead of real JSON. - Empty wrappers or meta-properties with no real purpose. ## Pre-return checklist 1. Do all `elementRef` values point to an existing element. 2. Does every `page` have a corresponding `elements[pageName]` entry. 3. Do logic fields return the correct type. 4. Are there no self-reference expressions. 5. Is the correct element chosen for the task. 6. Are there no unnecessary properties added. 7. Are all `type` strings exact-cased (e.g. `datepicker`, not `datePicker`)? 8. Do validators use `condition` (error when `true`) and `applyIf`, rather than `expression`, `visibleIf`, or other element-level field names? 9. Is every container's content attached to the referencing **column**, and does no element definition contain a `rows` / `children` / `items` array? 10. Do all column objects carry only `elementRef` (plus `rows` / `tabRows` where applicable), with no widths or other properties? --- ## AI: Element selection map Source: https://ngxviewbuilder.io/ai/element-selection-map # AI: Element selection map This page helps the agent decide which NGX View Builder element to use based on the user's intent. ## Quick reference - "dynamic table", "editable rows", "add row", "delete row" -> `dynamicTable` - "server-side table", "paging", "sorting", "filtering", "export", "row actions" -> `table` - "repeating block", "repeatable section" -> `dynamicPanel` - "single choice from a list" -> `select` or `radio` - "search in a large list" -> `autocomplete` - "multiple choices" -> `multiSelect` or `checkbox` - "yes/no toggle" -> `singleCheckbox`, `toggleSwitch`, or `toggleButton` - "handwritten signature" -> `signaturePad` - "time of day" -> `timePicker` - "quantity with +/- buttons" -> `numberStepper` ## Text and input elements | User intent | Choose | When not to choose | | --- | --- | --- | | A single short text value | `text` | Do not use for long descriptions | | Longer comment or notes | `textarea` | Do not use if rich content is needed | | Formatted content | `richText` | Do not use for plain single-line text | | Number | `number` | Do not use if it is only a text code | | Sliding numeric value | `slider` | Do not use for precise financial amounts | | Phone number | `phoneInput` | Do not use plain `text` if country-code UX is required | | Large searchable list | `autocomplete` | Do not use for small static lists | | File upload | `fileUpload` | Do not use for plain link text | | Triggering an action | `button` | Do not use for data input | | Code or JSON fragment | `textarea` | There is no `code` element type | | Small bounded quantity with +/- buttons | `numberStepper` | Do not use for large or precise ranges; prefer `number` | | Handwritten signature capture | `signaturePad` | Do not use for typed text | ## Choice elements | User intent | Choose | When not to choose | | --- | --- | --- | | Single choice from several options | `select` | If all options must always be visible, prefer `radio` | | Single choice with all options visible | `radio` | Do not use if there are very many options | | Multiple choices from a list | `multiSelect` | Do not use for a yes/no scenario | | Multiple choices as a checkbox group | `checkbox` | Do not use for a single boolean field | | Single boolean checkbox | `singleCheckbox` | Do not use if a button-style switch is desired | | Boolean toggle switch | `toggleSwitch` | Do not use if checkbox semantics are required | | Boolean button-style toggle | `toggleButton` | Do not use if a standard checkbox is needed | | Date selection | `datepicker` | Do not use for a date range | | Date range | `dateRange` | Do not use for a single date | | Time of day | `timePicker` | Combine with `datepicker` if both are needed | | Simple dropdown menu | `button` with `menuActions` | There is no `dropdown` element type; `select` is the closed choice control | | Always-visible scrollable selection list | `listBox` | Do not use if a compact closed control is preferred; use `select` | | Connected button row for 2-4 short options | `selectButton` | Do not use for long option lists; prefer `radio` or `select` | ## Tables and repeating data | User intent | Choose | Note | | --- | --- | --- | | User must add, delete, and edit rows inline | `dynamicTable` | This is not `table` | | Repeat an entire block with multiple fields | `dynamicPanel` | Better than `dynamicTable` if it is not a table | | Server-side table with paging, sorting, filtering, row actions | `table` | This is the main data grid | | Simple list or card grid display | `listGrid` | Not for an editable table | | Chart | `chart` | Not for a table or repeating input | ## Containers and UI structure | User intent | Choose | Note | | --- | --- | --- | | Simple block with inner fields | `panel` | Most common container | | Content divided into tabs | `tabs` or `tabsPro` | Do not use if multiple pages are sufficient | | Collapsible sections | `accordion` | Good for longer forms | | Step flow or progress | `progressFlow` | Not for a plain panel layout | | Modal-style content | `dialog` | Not as the primary page-level form container | | Two-zone layout with a divider | `splitter` | Only when two separate regions are needed | | Empty placeholder block for a drop zone | `emptyBlock` | Internal layout helper | | Visual separator | `divider` | Do not use for data storage | | Spacing | `spacer` | Do not use as a structural container | ## Display and media elements | User intent | Choose | Note | | --- | --- | --- | | Static HTML template | `customHtml` or `htmlSnippet` | Only when template HTML is truly needed | | Rich text viewer | `richTextViewer` | Not for editing | | Image | `image` | Not for file upload | | Video | `video` | Media viewing only | | External page or content | `iframe` | Only if the host allows it | | Avatar | `avatar` | Not for general images | | Icon | `icon` | Small UI symbol | | Navigation path | `breadcrumbs` | Navigation context | | Page heading | `pageTitle` | Hero or page top | | Badge | `badge` | Short status or label | | Informational card | `messageCard` | Message, warning, info | | Statistics card | `statsCard` | KPI or summary | | Toast notification | `toast` | Temporary notifications | | Completion percentage | `progressBar` | Not for step/page navigation; use `progressFlow` | ## Internal or system types The agent should not normally generate these directly as user form elements: - `row` - `column` - `visibility` - `choices` - `validators` - `dataSourceArguments` They belong to the internal model, property editing, or builder infrastructure. ## Keyword map | Prompt phrase | Choose | | --- | --- | | `dynamic table`, `editable rows`, `add row`, `delete row` | `dynamicTable` | | `repeating block`, `repeatable section`, `repeatable group` | `dynamicPanel` | | `data table`, `server-side table`, `paging`, `sorting`, `filtering` | `table` | | `search in list`, `searchable select`, `autocomplete` | `autocomplete` | | `multiple checkboxes`, `select multiple` | `checkbox` or `multiSelect` | | `yes/no`, `toggle`, `switch` | `singleCheckbox`, `toggleSwitch`, or `toggleButton` | ## If you are unsure between two options - `dynamicTable` vs `table`: does the user edit rows inline, or work with a datasource data grid? - `dynamicPanel` vs `dynamicTable`: does the data look more like a form/block or like a row-based table? - `select` vs `autocomplete`: are there few options, or many that require search? - `singleCheckbox` vs `toggleSwitch`: does checkbox semantics matter, or is a switch style preferred? --- ## AI: Logic and expression properties Source: https://ngxviewbuilder.io/ai/logic-and-expressions # AI: Logic and expression properties This page defines which expression fields the agent may use and which rules it must follow. ## Primary expression properties | Property | When to use | Must return | | --- | --- | --- | | `visibleIf` | Show an element (native field; return `false` to hide) | `true` or `false` | | `disableIf` | Make an element inactive | `true` or `false` | | `requireIf` | Make an element required | `true` or `false` | | `readonlyIf` | Make an element read-only | `true` or `false` | | `resetIf` | Clear an element under a condition | `true` or `false` | | `expression` | Compute and write a value | `string`, `number`, `boolean`, `array`, or `object` | | `validators[].applyIf` | Enable a validator conditionally | `true` or `false` | | `validators[].condition` | Failing check, error shown while `true` | `true` or `false` | ## Other expression usage points The agent must know that expression logic does not live only in the main fields. ### Data source parameters The field is called `params`, never `paramMap`: - an element's `dataSource.params[*].value`, shaped `[{ "name": ..., "value": ... }]` - an action's `params[*].value`, the same shape - a `table`'s own lazy-load request params, which are different: `[{ "paramName": ..., "paramValue": ... }]` - `{placeholder}` tokens inside a REST source's `params.url` and `params.body` These can use expression fragments such as: ``` {personCode} {row.id} {__variables.route.mode} ``` ### Table columns `table.columnsConfig[*]` can use: - `visibleIf` - `controlActiveIf` - `controlEnabledIf` Inside a column of `type: "element"`, the hosted element's own texts, templates, and event params resolve against the row it renders: `{row.field}`, a bare `{field}` for a sibling of the same row, `{index}`, and `{value}`. The same tokens work in a `dynamicTable` cell and a `dynamicPanel` row. ### Event and action logic Expressions are often used in actions in: - `params[*].value` - `condition`, which gates whether the action runs at all (per row for a table row action) - `setValueValue` when `setValueMode` is `template` or `expression` ## When to use `logicExecutionMode` If there is a dependency between fields, especially when `expression` is used, you typically need: ```json { "logicExecutionMode": "onChange" } ``` This is especially important when: - one field fills another - one field changes another's required state - one field hides or disables another ## Core writing rules - Use only NGX View Builder expression syntax, not full `JavaScript`. - Read field values via `{fieldName}` or `{object.field}`. - Use quotes for string values. - Do not add `toNumber(...)` by reflex. Number elements already hold numbers, so `{price} * {quantity}` works as written. Reach for it only when the value genuinely arrives as text: a `text` element, a data source field, a select whose values are strings, or a number element with `valueStorageType: "string"`. - Use `isEmpty(...)` and `notEmpty(...)` for empty checks. - Use `contains`, `containsAny`, `containsAll`, and `len` for arrays. ## Correct examples ### Auto-fill ```json { "name": "lastName", "type": "text", "expression": "{firstName} == 'John' ? 'Doe' : ''", "logicExecutionMode": "onChange" } ``` ### Conditional required ```json { "name": "companyCode", "type": "text", "requireIf": "{personType} == 'company'", "logicExecutionMode": "onChange" } ``` ### Show/hide ```json { "name": "vatCode", "type": "text", "visibleIf": "{country} == 'LT'" } ``` ### Calculation ```json { "name": "totalAmount", "type": "number", "expression": "{price} * {quantity}", "logicExecutionMode": "onChange" } ``` ## Bad examples ### Self-reference expression Incorrect: ```json { "name": "el2", "expression": "{el1} == 'John' ? 'Doe' : {el2}" } ``` Why it is wrong: - the element references itself - this leads to unstable or non-functioning logic Correct: ```json { "name": "el2", "expression": "{el1} == 'John' ? 'Doe' : ''", "logicExecutionMode": "onChange" } ``` ### Wrong type for a boolean property Incorrect: ```json { "visibleIf": "'yes'" } ``` `visibleIf` must return `true` or `false`, not a string. ### Reference to a non-existent field Incorrect: ```json { "disableIf": "{customerType} == 'vip'" } ``` if `customerType` does not exist in the form. ## Expression property map by need | Need | Property | | --- | --- | | Show/hide a field | `visibleIf` (return `false` to hide) | | Show but prevent editing | `disableIf` or `readonlyIf` | | Make required only in certain cases | `requireIf` | | Reset value when condition changes | `resetIf` | | Automatically compute a value | `expression` | | Enable a validator conditionally | `validators[].applyIf` | | Flag a custom validation error | `validators[].condition` (error while `true`) | ## Working with arrays and element metadata Conditions inside collection functions are written unquoted and evaluated once per entry. Entry fields are referenced bare, everything else in the view stays reachable through `{...}`: ```text countInArray({tasks}, status == "OPEN") filterArray({users}, role == {__variables.requiredRole}) findInArray({products}, id == {selectedId}) existsInArray({items}, status == "ACTIVE") sumArray({orderItems}, price) joinInArray(mapArray(filterArray({tasks}, status == "OPEN"), title), "", ", ") ``` Also available: `avgArray`, `getFirst(source, condition?)`, `getLast(source, condition?)`. `countInArray` still accepts a plain field selector, so older structures keep working. For element metadata rather than values: | Call | Returns | | --- | --- | | `getValue({select1})` | stored value, e.g. `OPEN` | | `getLabel({select1})` | displayed option label, e.g. `Open issue` | | `getLabel("select1", "CLOSED")` | label of a specific value | | `getElementProperty("el1", "label")` | any configured property, nested keys allowed | | `getProp({el1}, "placeholder")` | alias of `getElementProperty` | | `translate({row.status})` | value translated through `localization.texts[]` | | `currentLanguage()` | active language code | In `getLabel`, `getValue`, and `getProp` the token names the element, it is not replaced by that element's value. ## Writing values from an expression | Call | Does | | --- | --- | | `setValue({variable1}, value)` | writes into a variable or an element data path | | `sumValue({variable1}, value)` | adds a number to the current value, returns the new total | | `pushValue({variable1}, value)` | appends to the target's array | | `flattenArray(source)` | flattens nested arrays into one level | `setVar`, `sumVar`, and `pushVar` are the same calls with the target always read as a variable name. `addValue` is an alias of `sumValue`. Shorthand, where the leftmost token is the target and the right side reads normally: ```text {variable1} = {row.column3} + 40 {variable1} += {row.column3} ``` Rules for the agent: - The first argument names the target, do not replace it with a value. - `setValue` is safe in a recalculated expression, writing an unchanged value does nothing. - `sumValue` and `pushValue` add again on every run, so put them in an action, never in `visibleIf`, `disableIf`, or an `expression` property. - For a table inside a dynamic panel prefer one aggregate over accumulation: `setValue({variable1}, sumArray({el1}, el5[].column3))`. - A published column total is read as `{el4.column1-total}`, always in braces. ## Contexts the agent may encounter ### General form context - `{fieldName}` - `{nested.field}` - `{__variables.*}` ### Dynamic blocks and tables - `{row.field}` inside a `table` cell, a `dynamicTable` row, a row action or a column template - `{panel.field}` inside a `dynamicPanel` entry - `{item.field}` for the candidate option in `filterOptionsBy` - `{index}` and `{value}` inside a cell - a published `dynamicTable` column total, `{el1.column4-total}` ### A table's own live state A `table` publishes its state so other elements can read it. `__table..*` is that table, bare `__table.*` is the last one touched: `rows` `allRows` `rowCount` `totalRecords` `page` `size` `sortField` `sortDirection` `quickSearch` `detailedFilters` `request` `selectedRows` `selectedKeys` `selectedItems` `selectedCount` `selectedRow` `selectedRowKey` `selectedRowIndex` `activeRow` `activeRowKey` ```text {__table.el2.selectedCount} > 0 notEmpty({__table.el2.selectedRowKey}) {__table.el2.selectedRow}.column2 ``` This is what drives selection-aware buttons, counters and master/detail screens. Full examples: [Verified examples, 6.5 and 6.6](https://ngxviewbuilder.io/ai/examples#_6-5-the-table-s-live-state-table). In such cases, do not guess. If `table` or `dynamicPanel` is used, consult: - [Expressions](https://ngxviewbuilder.io/creators/expressions) - [Tables & lists](https://ngxviewbuilder.io/creators/elements/tables) ## When to avoid `expression` - When the user only wants a static default text. - When `defaultValue` is sufficient. - When you only need to show or hide a section, not compute new data. ## Final checklist 1. Does the condition reference existing fields. 2. Do boolean properties return a boolean value. 3. Does `expression` not return the wrong type. 4. Is there no self-reference. 5. Is `logicExecutionMode: "onChange"` added where needed. --- ## AI: Element rules and value shapes Source: https://ngxviewbuilder.io/ai/element-rules # AI: Element rules and value shapes This page is an AI-oriented summary of the primary NGX View Builder elements. For the full property list always consult: - [Element catalog](https://ngxviewbuilder.io/creators/elements/) - [Canonical properties reference](https://ngxviewbuilder.io/ai/properties-reference) ## General rules for all elements - Every user-facing element must have `name`, `label`, and `type`. - `name` must be unique. - If an element participates in logic, its `name` must be stable and meaningful. - Do not use properties that are not found in the documentation or catalog. ## Input elements ### `text` - Use for short text. - Typical value shape: `string`. - Common properties: `placeholder`, `defaultValue`, `required`, `expression`. ### `textarea` - Use for longer text, comments, and descriptions. - Value shape: `string`. - Do not use if formatted rich content is needed. ### `number` - Use for numbers, amounts, and quantities. - Value shape: `number` or `string`, depending on `valueStorageType`. - Do not wrap it in `toNumber(...)` in calculations. A `number` element already stores a number, so `{price} * {quantity}` is correct and `toNumber({price}) * toNumber({quantity})` only adds noise. The exception is `valueStorageType: "string"`, which really does store text. ### `slider` - Use for ranges or bounded numeric values with a slider. - Value shape: `number` or `string`. - Do not use for precise manual financial input. ### `numberStepper` - Use for small bounded quantities with increment/decrement buttons (passengers, rooms, items). - Value shape: `number`. - Use `min`/`max` to bound it. Do not use for large or precise numeric ranges; prefer `number`. ### `signaturePad` - Use to capture a handwritten signature (mouse or touch). - Value shape: signature image data (per `exportFormat`, e.g. `png`/`jpeg`), keyed under the element's `name`. - Do not use as a substitute for typed text input. ### `phoneInput` - Use for phone numbers with country code. - Value shape is often an object with country and number parts. - Do not use plain `text` if a standard phone UX is required. ### `autocomplete` - Use for a large list of options with search. - Value shape depends on the option mapping. - Often requires a datasource or options mapping. ### `fileUpload` - Use for file uploads. - Value shape: the upload endpoint's response object, stored verbatim (or an array when `multiple`), never file bytes/base64. Depends on `fileKeyField`/`fileNameField`/`fileTypeField`/`fileSizeField`. - Do not treat it as a plain text URL field. - Consulting mode: for the exact upload/download/delete request and response contract, see [Properties reference](https://ngxviewbuilder.io/ai/properties-reference#fileupload) and [File upload requests](https://ngxviewbuilder.io/developers/data-sources#file-upload-requests). ### `button` - Use to trigger an action. - Not intended for form value storage. - Common properties: `variant`, `events`, `actions`, `disabled`. ### `richText` / `richTextViewer` - `richText` edits formatted HTML; `richTextViewer` only renders it. - Value shape is an HTML `string`. - There is no `code` element type. For a code or JSON fragment use `textarea`, or `customHtml` / `htmlSnippet` for read-only markup. ## Choice elements ### `select` - Single choice from a list. - Value shape: single value. - Works with static or datasource options. ### `multiSelect` - Multiple choices from a list. - Value shape: array. - Suitable when there are many options and a compact UI is needed. ### `radio` - Single choice when all options must be visible at once. - Value shape: single value. ### `checkbox` - Multiple choices as a checkbox group. - Value shape: array. - Do not use for a single boolean field. ### `singleCheckbox` - Single boolean checkbox. - Value shape: `boolean`. ### `toggleSwitch` - Single boolean switch. - Value shape: `boolean`. - Good for active/inactive scenarios. ### `toggleButton` - Boolean or small-choice button-style control. - Value shape is typically `boolean`. ### `datepicker` - **Exact type string: `"datepicker"`** (all lowercase, never `datePicker`, `DatePicker`, or `date-picker`). - Single date. - Value shape is typically a date string or documented date format. - Do not use for a date range. Canonical example: ```json { "name": "birthDate", "type": "datepicker", "label": "Date of Birth", "placeholder": "Select date", "required": true, "validators": [ { "type": "maxDate", "value": "today", "message": "Cannot be in the future" } ] } ``` ### `dateRange` - Date range. - Value shape: ```json { "dateFrom": "2026-03-01", "dateTo": "2026-03-09" } ``` - Do not use for a single date. ### `timePicker` - Selects a time of day. - Value shape is typically a time string. - Combine with `datepicker` when both a date and a time are needed, or use a `text` element with a date-time input mask for single-field entry. ### `dropdown` does not exist - There is no `dropdown` element type. `select` is the closed dropdown control. - For an action menu use `button` with `menuActions`, or `table.rowActions` with `rowActionsDisplayMode: "dropdown"`. ### `listBox` - Always-visible scrollable selection list, single or multiple via `selectionMode`. - Value shape: single value (`selectionMode: "single"`) or array (`selectionMode: "multiple"`). - Use when choosing is the primary task on screen; otherwise prefer `select`. ### `selectButton` - A row of connected buttons, a visual alternative to `radio` for 2-4 short options. - Value shape: single value, or array when `multiple` is `true`. - Do not use for long option lists; prefer `radio` or `select`. ## Table and repeating data elements ### `dynamicTable` - Use for editable rows. - The user must be able to add, delete, and edit rows. - Value shape is typically: array of objects. - This is not a server-side data grid. ### `table` - Use for displaying data from a datasource. - Supports `sorting`, `filtering`, `paging`, `rowActions`, and `export`. - Requires `columnsConfig` and often datasource properties. - Use `key`, not `name`, for `columnsConfig[*]` columns. - A column either prints text (`type: "text"`) or hosts a real element (`type: "element"` plus `elementType` and `element`). Use the element form whenever the cell needs a control, a badge, a button, or an event. - Server-side paging/search (`lazyLoad: true` + a `TABLE-POST` datasource method) has its own request/response contract, so do not invent one. Consulting mode: see [Properties reference](https://ngxviewbuilder.io/ai/properties-reference#table) and [Table: server-side paging & filtering](https://ngxviewbuilder.io/developers/data-sources#table-server-side-paging-filtering-table-post). - If `key` is empty or absent, the column will not be rendered at runtime. - This is not an editable `dynamicTable`. ### `listGrid` - Use for displaying a list or card grid. - Do not use if full table functionality is needed. ### `chart` - Use for graphical visualization. - Do not use as a data input element. ## Container and layout elements ### `page` - Every `pages[*]` entry must have a corresponding `elements[pageName]` entry with `type: "page"`. - `page` itself is the top-level form page. ### `panel` - The most common container for grouping fields. - Suitable for sections with an inner layout. ### `dynamicPanel` - Use for repeating sets of blocks. - Value shape is typically: array of objects. - Suitable for addresses, family members, and education records. ### `tabs` and `tabsPro` - Use to divide content into tabs. - Suitable when multiple clearly separated areas are needed on a single page. ### `accordion` - Use for collapsible sections. - Suitable for longer forms. ### `progressFlow` - Use to display steps or progress. - Not for primary data input. ### `dialog` - Use for modal content. - Not every form needs `dialog*` settings. ### `splitter` - Use for a two-zone layout with a divider. ### `emptyBlock` - Internal helper placeholder for layout scenarios. ### `row` and `column` - These are internal layout concepts. - The agent typically does not generate them as top-level elements in the `elements` map. ## Display and media elements ### `richText` - Formatted content or a longer rich editor scenario. - Value shape: `string`. ### `richTextViewer` - For rich text viewing. - Not for editing. ### `customHtml` - Use only when template HTML is genuinely needed, based on documented NGX View Builder capability. - Do not use as an excuse to generate custom Angular. ### `htmlSnippet` - Use for short HTML fragments. ### `image` - For displaying images. ### `video` - For displaying video. ### `iframe` - For embedding external content. - Suitable only if the host allows it. ### `avatar` - For small user or object representations. ### `icon` - For a UI icon. ### `divider` - For visual separation. ### `spacer` - For spacing. ### `breadcrumbs` - For navigation path. ### `pageTitle` - For page or section headings. ### `badge` - For a short label or status. ### `messageCard` - For an informational, success, or warning card. ### `statsCard` - For KPI or summary blocks. ### `toast` - For temporary notifications. - Often tied to actions, not permanent layout. ### `progressBar` - Completion indicator; value 0-100 (or a custom `max`) from `expression`, `defaultValue`, or a bound datasource. - Value shape: `number`. - Not for step/page navigation; use `progressFlow` for that. ## Validators Validators belong in the `validators` array on any element. The exact interface is: ```typescript interface IValidator { type: string; // validator type (required) value?: string | number; // threshold (minLength, maxLength, min, max, minDate, maxDate, etc.) message?: string; // error text shown to user condition?: string; // expression, the FAILING check; the error is shown while it evaluates to true applyIf?: string; // expression, validator only runs while this evaluates to true } ``` **Field names that do NOT exist on `IValidator`: `expression`, `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, `resetIf`.** Those are element-level logic fields. Never place them inside a validator object. When writing a custom `condition`, express the failing state, for example: ```json { "type": "custom", "condition": "dateDiffDays({startDate}, {endDate}) < 1", "message": "End date must be after start date" } ``` Correct usage: ```json { "name": "firstName", "type": "text", "label": "First Name", "requireIf": "{step} == 'personal'", "validators": [ { "type": "minLength", "value": 2, "message": "At least 2 characters required" }, { "type": "maxLength", "value": 64, "message": "Must not exceed 64 characters", "applyIf": "{firstName} != ''" } ] } ``` Wrong, do not do this: ```json { "validators": [ { "type": "minLength", "expression": "{firstName}.length >= 2", "visibleIf": "{skip} == true" } ] } ``` ## Internal and system types Do not normally generate these as user form elements unless working with builder infrastructure: - `visibility` - `choices` - `validators` - `dataSourceArguments` ## Per-element AI checklist Before generating an element, the agent must answer: 1. What is the purpose of this element. 2. What must the value shape be. 3. Is a datasource needed. 4. Are logic properties needed. 5. Does the user want input, display, repetition, or a data grid. --- ## AI: Canonical properties reference Source: https://ngxviewbuilder.io/ai/properties-reference # AI: Canonical properties reference This is the authoritative property reference for AI JSON generation. Only properties listed here are supported. Do not invent or guess properties not found in this document. --- ## Base properties (all elements) Every element must have these three fields: ```json { "name": "fieldName", "label": "Field Label", "type": "text" } ``` Additional base properties available on all element types: | Property | Type | Description | |---|---|---| | `description` | `string` | Subtitle or help text shown below the label | | `hidden` | `boolean` | Statically hides the element | | `width` | `string` | Width (e.g. `"100%"`, `"300px"`) | | `tabletWidth` | `string` | Width on tablet | | `mobileWidth` | `string` | Width on mobile | | `dependsOn` | `string[]` | Limit expression recalculation to these field names | | `logicExecutionMode` | `"onBlur" \| "onChange" \| "onInput"` | When logic/expression is re-evaluated | | `validationExecutionMode` | `"onBlur" \| "onChange" \| "onInput"` | When validators are re-evaluated | | `status` | `"default" \| "readonly"` | Interaction status override | | `labelTooltip` | `string` | Tooltip shown on label hover | | `fitContent` | `boolean` | Width fits content | --- ## Logic fields (element level only, never inside validators) These fields accept JEXL expressions using `{fieldName}` placeholder syntax. | Field | Return type | Behavior | |---|---|---| | `visibleIf` | `boolean` | Element visible when `true` | | `disableIf` | `boolean` | Element disabled when `true` | | `requireIf` | `boolean` | Element required when `true` | | `readonlyIf` | `boolean` | Element readonly when `true` | | `resetIf` | `boolean` | Element value cleared when `true` | | `expression` | `any` | Result is set as element value; also for `setElementProperty` calls | **All logic fields must contain valid JEXL expressions only.** **There is no `hideIf`, and no `readOnlyIf` with a capital O.** These six keys are the entire list and they are matched exactly. Anything else is an ordinary property as far as the runtime is concerned: it is stored, it is never evaluated, and no dependency is registered for it. What you get is a form that looks correct in the JSON and does nothing at all, with no error anywhere to point at. Write the visible condition rather than the hidden one, because `visibleIf` states when the element **is** shown, so a hide condition has to be inverted, not renamed. ### Execution mode | Field | Default | Behavior | |---|---|---| | `logicExecutionMode` | `onBlur` | When the logic fields above are re-evaluated | | `validationExecutionMode` | `onBlur` | When validators are re-evaluated | Both fall back to `onBlur` when the property is missing, so logic only re-runs after the field loses focus. To anyone typing in the form that reads as broken logic, so set `onChange` on every element whose logic or validation has to follow the typing, unless another mode was asked for. --- ## Static state fields | Field | Type | Description | |---|---|---| | `required` | `boolean` | Always required | | `disabled` | `boolean` | Always disabled | | `readOnly` | `boolean` | Always read-only | --- ## Validators `validators` is an array of `IValidator` objects. Exact interface: ```typescript interface IValidator { type?: string; // validator type (required) message?: string; // error message shown to user value?: string | number; // threshold (minLength, maxLength, min, max, minDate, maxDate) condition?: string; // JEXL, the FAILING check; error shown while this evaluates to true applyIf?: string; // JEXL, validator runs only when this is truthy } ``` **Forbidden inside validators: `expression`, `visibleIf`, `requireIf`, `disableIf`, `readonlyIf`, `resetIf`.** ```json { "validators": [ { "type": "minLength", "value": 3, "message": "At least 3 characters" }, { "type": "maxLength", "value": 100, "message": "Too long", "applyIf": "notEmpty({name})" }, { "type": "min", "value": 0, "message": "Cannot be negative" }, { "type": "max", "value": 999, "message": "Too large" }, { "type": "minDate", "value": "today", "message": "Cannot be in the past" }, { "type": "maxDate", "value": "today", "message": "Cannot be in the future" } ] } ``` --- ## Events / Actions `events` is an array of `IElementActionConfig` objects. Key fields: | Field | Type | Values / Notes | |---|---|---| | `trigger` | `string` | `click` `submit` `input` `change` `blur` `focus` `beforeLoad` `onLoad` `afterLoad` `always`. Prefer a named trigger; `always` skips trigger filtering entirely and is only for host elements whose events are not gestures (a router outlet's `activate`/`deactivate`), so on a normal element it fires on load and on every gesture. | | `type` | `string` | `navigate` `dataSource` `setValue` `setOptions` `reloadElements` `toast` `dialog` `validate` `submit` | | `label` | `string` | Button label (when rendered as action button) | | `icon` | `string` | Icon name | | `condition` | `string` | JEXL, action runs only when truthy | | `validateForm` | `boolean` | Validate before executing | | `confirmEnabled` | `boolean` | Show confirmation dialog before action | | `confirmTitle` | `string` | Confirm dialog title | | `confirmMessage` | `string` | Confirm dialog body | **By action type:** `navigate`: `navigateTo` (URL/route), `openInNewTab` (boolean) `dataSource`: `dataSourceName`, `responseMode` (`none` `download` `setValue`), `responseDataPath`, `responseTargetElement`, `reloadCurrentElementAfterSuccess`, `reloadOnReturnElementNames` `toast`: `toastTitle`, `toastMessage`, `toastVariant` (`error` `warning` `info` `success`), `toastPosition` (`top-left` `top-center` `top-right` `bottom-left` `bottom-center` `bottom-right`), `toastAutoHide`, `toastAutoHideMs` `dialog`: `dialogName`, `dialogOperation` (`open` `close` `toggle`) `setValue`: `setValueTargetPath`, `setValueMode` (`contextPath` `template` `json`), `setValueValue` `setOptions`: `setOptionsTargetElement`, `setOptionsMode` (`contextPath` `template` `json`), `setOptionsValue` `reloadElements`: `reloadElementNames` (comma-separated) `validate`: no extra fields. Validates the whole form and shows field errors, same as the built-in Validate toolbar button `submit`: no extra fields. Validates the form, then fires `onComplete` with `{ isValid, data, issues }`, same as the built-in Submit toolbar button Button appearance for action: `buttonVariant` (`filled` `outlined` `text`), `buttonTone` (`primary` `risk` `neutral`), `hideText`, `useCustomButtonStyle` --- ## Per-type additional properties ### `text` | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | | | `minlength` | `number` | | | `maxlength` | `number` | | | `showMaxLengthCounter` | `boolean` | Show counter when maxlength is set | | `pattern` | `string` | Regex pattern | | `maskType` | `string` | Exactly: `none` `phoneIntl` `date` `time` `dateTime` `digits` `custom`. No other mask names exist | | `maskPattern` | `string` | Custom mask (when maskType is `custom`) | | `inputMode` | `string` | `text` `search` `email` `tel` `url` `numeric` `decimal` | | `spellcheck` | `boolean` | | | `autocomplete` | `string` | Browser autocomplete hint | | `requiredMessage` | `string` | Custom required error message | ### `textarea` Same as `text` plus: | Property | Type | Notes | |---|---|---| | `rows` | `number` | Visible row count | ### `number` | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | | | `min` | `number` | Minimum value | | `max` | `number` | Maximum value | | `step` | `number` | Increment step | | `inputMode` | `string` | `numeric` `decimal` | | `valueStorageType` | `string` | `number` (default) or `string` | | `visualFormatEnabled` | `boolean` | Enable visual number formatting | | `visualFormatLocale` | `string` | Locale for formatting (e.g. `"lt-LT"`) | | `visualFormatUseGrouping` | `boolean` | Thousands separator | | `visualFormatMinFractionDigits` | `number` | | | `visualFormatMaxFractionDigits` | `number` | | | `requiredMessage` | `string` | | ### `datepicker` **Exact type string: `"datepicker"` (all lowercase)** | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | | | `pickerMode` | `string` | `date` (default) or `datetime` | | `includeSeconds` | `boolean` | For datetime mode | | `format` | `string` | Output format. Exactly one of `""` (locale) `YYYY-MM-DD` `DD/MM/YYYY` `MM/DD/YYYY` `DD.MM.YYYY` `YYYY/MM/DD` `custom`. Lowercase Angular-style patterns such as `yyyy-MM-dd` are **not** valid | | `customFormat` | `string` | The pattern used when `format` is `custom` | | `minValue` | `string` | Min selectable date (JEXL or static) | | `maxValue` | `string` | Max selectable date (JEXL or static) | | `requiredMessage` | `string` | | ### `dateRange` | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | | | `format` | `string` | | | `minValue` | `string` | | | `maxValue` | `string` | | | `requiredMessage` | `string` | | Value shape: `{ "dateFrom": "YYYY-MM-DD", "dateTo": "YYYY-MM-DD" }` ### `select` / `multiSelect` / `radio` / `checkbox` / `autocomplete` | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | Real on `autocomplete`, `listBox`, `selectButton`. On `select` / `multiSelect` it sets **only the search box hint** inside the open dropdown, and only when `showSearch` is on; the closed-state "Select..." text comes from the UI translation `select.placeholder` / `multiSelect.placeholder`, not from this property | | `options` | `IOption[]` | Static options: `[{ "value": "x", "label": "X" }]` | | `dataSource` | `IElementDataSource` | Dynamic options from datasource | | `showSearch` | `boolean` | Search box in dropdown (select) | | `strictOptions` | `boolean` | Value must be from options list | | `filterIfEqual` | `string` | Filter options when datasource field equals value | | `filterIfNotEqual` | `string` | Filter options when datasource field not equals value | | `optionTemplate` | `string` | Inline HTML template for each option | | `optionTemplateName` | `string` | Template name reference | | `showInline` | `boolean` | Lay the choices out in a row (`radio`, `checkbox`) | | `requiredMessage` | `string` | | Value shapes: - `select` / `radio` / `autocomplete`: single value - `multiSelect` / `checkbox`: array ### `singleCheckbox` / `toggleSwitch` / `toggleButton` Value shape: `boolean` | Property | Type | Notes | |---|---|---| | `checkboxLabel` | `string` | Text rendered **next to the box** (`singleCheckbox`). `label` is the field label above it; for a bare checkbox row set `label` to `""` and put the wording here | | `checkedValue` | `any` | Value when checked (`toggleButton` only) | | `uncheckedValue` | `any` | Value when unchecked (`toggleButton` only) | `singleCheckbox` has no `checkedValue` / `uncheckedValue`. It always stores `true` / `false`. ### `phoneInput` | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | | | `defaultCountryCode` | `string` | ISO code of the preselected country: `LT` `LV` `EE` `PL` `DE` `SE` `NO` `DK` `FI` `GB` `IE` `US` | | `allowedCountryCodes` | `string[]` | Restrict the country dropdown to these ISO codes | | `maxlength` | `number` | | | `autocomplete` | `string` | | | `requiredMessage` | `string` | | `phoneInput` has its own country dropdown. It has **no** `maskType`, `maskPattern` or `inputMode`, and the dial code is not set via `defaultValue`. ### `button` | Property | Type | Notes | |---|---|---| | `text` | `string` | Button text | | `icon` | `string` | Icon name | | `iconPosition` | `string` | `left` `right` `top` `bottom` | | `iconOnly` | `boolean` | Show icon only | | `variant` | `string` | `solid` `outline` `text` | | `tone` | `string` | `primary` `neutral` `success` `info` `warning` `risk` | | `size` | `string` | `small` `normal` `large` | | `loading` | `boolean` | Show loading spinner | | `badge` | `string` | Badge text | | `events` | `IElementActionConfig[]` | Primary click actions | | `menuActions` | `IElementActionConfig[]` | Dropdown menu actions | ### `fileUpload` | Property | Type | Notes | |---|---|---| | `accept` | `string` | Accepted MIME types or extensions, e.g. `.pdf,image/*` | | `multiple` | `boolean` | Allow multiple files | | `maxFiles` | `number` | Max number of attached files (default `5`) | | `maxFileSizeMb` | `number` | Max file size **in megabytes**, not bytes | | `uploadDataSourceName` / `downloadDataSourceName` / `deleteDataSourceName` | `string` | Names of REST data sources; empty `uploadDataSourceName` means files never leave the browser (local metadata only) | | `uploadFormFieldName` | `string` | `multipart/form-data` field name for the file (default `file`). Upload always sends single-field `FormData`, never JSON/base64 | | `fileKeyField` / `fileNameField` / `fileTypeField` / `fileSizeField` | `string` | Field names read from the server's response object (defaults `key`, `name`, `contentType`, `size`; the older `fil_key`/`fil_name`/`fil_content_type`/`fil_size` names are still recognized as fallback aliases) | Value shape: the upload response object stored verbatim (or an array of them when `multiple`), keyed under the element's `name`, never file bytes/base64. Full request/response wire contract: [File upload requests](https://ngxviewbuilder.io/developers/data-sources#file-upload-requests). ### `page` The complete list. A `page` element accepts nothing else. | Property | Type | Notes | |---|---|---| | `name` | `string` | Must match the `pages[*].name` entry | | `label` | `string` | Page / step title | | `description` | `string` | Subtitle | | `type` | `"page"` | Required | | `hideHeader` | `boolean` | Hide the page title block | | `removeBackgraund` | `boolean` | Drop the page background. Spelled exactly like this, the typo is part of the schema | | `pageBackgroundColor` | `string` | | | `pagePadding` | `string` | | | `mobilePadding` | `string` | | | `visibleIf` / `disableIf` / `readonlyIf` | `string` | JEXL | A page's children are **not** listed here. They live in `pages[*].rows`; see [Layout model](https://ngxviewbuilder.io/ai/layout-model). ### `panel` | Property | Type | Notes | |---|---|---| | `headerTemplate` | `string` | Inline HTML for panel header | | `headerTemplateName` | `string` | Template name reference | | `maxHeight` | `string` | Max panel height | | `panelPadding` | `string` | Internal padding | | `mobilePadding` | `string` | Mobile padding | | `panelRadius` | `string` | Border radius | | `panelShadow` | `string` | Box shadow CSS | | `showBorder` | `boolean` | Show panel border | | `panelBorderWidth` | `string` | Border width (when showBorder) | | `panelBorderColor` | `string` | Border color (when showBorder) | | `panelBackgroundColor` | `string` | Background color | | `titleUnderline` | `boolean` | Show underline below title | | `contentJustify` | `string` | Flexbox justify: `flex-start` `center` `flex-end` `space-between` `space-around` `space-evenly` | | `contentAlign` | `string` | Flexbox align: `flex-start` `center` `flex-end` `stretch` | | `contentGap` | `string` | Gap between child elements | | `titleUnderlineColor` | `string` | Underline color (when `titleUnderline`) | | `titleUnderlineWidth` | `string` | Underline thickness (when `titleUnderline`) | | `resetChildrenOnHide` | `boolean` | Clear descendant field values when the panel becomes hidden | **A panel never lists its children.** There is no `rows`, `columns`, `children`, `elements` or `items` property on `panel`. Child fields are attached in the layout tree, on the column that references the panel. See [Layout model](https://ngxviewbuilder.io/ai/layout-model#nesting-the-mistake-that-breaks-everything). ### `dynamicPanel` Same as `panel` plus repeatable container behavior (`addRowButtonText`, `removeRowButtonText`, `emptyMessage`, `disallowAddRows`, `disallowDeleteRows`, `maxRows`, `confirmRowDeletion`, `hideHeader`). Value shape: array of objects. Children are attached the same way as for `panel`, via `column.rows`. ### `dynamicTable` | Property | Type | Notes | |---|---|---| | `emptyMessage` | `string` | Message when no rows | | `addRowButtonText` | `string` | Add row button label | | `itemsPath` | `string` | Data path for pre-populating rows | | `disallowAddRows` | `boolean` | | | `disallowDeleteRows` | `boolean` | | | `maxRows` | `number` | | | `confirmRowDeletion` | `boolean` | | | `confirmDeleteTitle` | `string` | | | `confirmDeleteMessage` | `string` | | | `confirmDeleteConfirmLabel` | `string` | | | `confirmDeleteCancelLabel` | `string` | | | `hideRowIf` | `string` | JEXL, hides individual rows | | `columns` | `IDynamicTableColumn[]` | Column definitions | | `dataSource` | `IElementDataSource` | Pre-populate from datasource | Column-level totals: | Property | Type | Notes | |---|---|---| | `columns[*].useTotals` | `boolean` | Sum in the footer row, also published to data | | `columns[*].totalToData` | `boolean` | Publish the sum without a footer row | A published sum lands in the data as `.-total`, for example `el4.column1-total`, and is read in expressions as `{el4.column1-total}`. Inside a dynamic panel the key carries the entry index: `el1[0].el5.column3-total`. Value shape: array of objects. ### `table` `columnsConfig` entries use `key` (NOT `name`). Minimum column: ```json { "key": "firstName", "label": "First Name", "showInTable": true } ``` Column type values the builder offers: `text`, `date`, `dateTime`, `number`, and `element`. The renderer still understands `boolean` `html` `status` `toggleSwitch` `singleCheckbox` so older structures keep working, but do not author new columns with them; use `element` with the matching `elementType` instead. `date`, `dateTime`, and `number` format the raw value in place, no template or cell element needed: ```json { "key": "created", "label": "Created", "type": "dateTime", "dateIncludeSeconds": true, "showInTable": true } ``` | Property | Type | Notes | |---|---|---| | `formatLocale` | `string` | `lt-LT`, `en-US`. Empty follows the view locale | | `dateFormatPattern` | `string` | `yyyy-MM-dd HH:mm:ss`. Tokens `yyyy yy MM dd HH hh mm ss SSS a`. Wins over the locale format | | `dateIncludeSeconds` | `boolean` | `dateTime` only | | `numberMinFractionDigits` | `number` | Decimal places, minimum | | `numberMaxFractionDigits` | `number` | Decimal places, maximum | | `numberUseGrouping` | `boolean` | Thousands separator, defaults to `true` | A column of `type: "element"` renders a real element in every row: ```json { "key": "status", "label": "Status", "type": "element", "showInTable": true, "elementType": "select", "element": { "options": [{ "label": "New", "value": "new" }, { "label": "Done", "value": "done" }], "events": [ { "trigger": "change", "type": "dataSource", "dataSourceName": "saveStatus", "params": [ { "name": "id", "value": "{row.id}" }, { "name": "status", "value": "{value}" } ] } ] } } ``` `elementType` is the element type id (`select`, `button`, `badge`, `datepicker`, `progressBar`, `customHtml`, `richTextViewer`, and so on). Use `richTextViewer` when the row field already holds HTML and only needs rendering. `element` is that element's own configuration, the same keys it would have as a page element. Inside it, `{row.*}`, a bare sibling field name, `{index}`, and `{value}` all resolve against the row the cell belongs to. | Property | Type | Notes | |---|---|---| | `columnsConfig` | `ITableColumnConfig[]` | Column definitions, use `key` not `name` | | `columnsConfig[*].elementType` | `string` | Element type rendered in the cells, when `type` is `element` | | `columnsConfig[*].element` | `object` | Configuration of that hosted element (options, dataSource, events, validators) | | `dataSource` | `IElementDataSource` | The table's source, for both client-side and server-side tables. With `lazyLoad: false` it must return **all** rows in one response; with `lazyLoad: true` it must accept the `TABLE-POST` paging/filter contract below | | `tableDataSourceName` | `string` | Legacy server-side binding, still read for older structures. New structures put the source in `dataSource` | | `lazyLoad` | `boolean` | `true` = server-side paging/sort/search per request; `false` = load everything once | | `tableItemsPath` / `tableTotalPath` | `string` | Response paths for rows / total count, both optional. Common shapes (`items`/`data`/`results`/`rows`, `total`/`totalCount`/`totalRecords`/`count`/`cnt`) are auto-detected | | `pageSize` | `number` | Default page size | | `rowActions` | `ITableRowActionConfig[]` | Row action buttons. These go through the generic action/data-source pipeline, not this contract | **Do not use `ITableDataSourceConfig`.** It exists in the type definitions but is dead code, never wired to anything; the real per-table binding is the flat `tableDataSourceName`/`tableItemsPath`/`tableTotalPath` trio above. When `lazyLoad: true` and the data source's `method` is the literal string `TABLE-POST` (sent over the wire as a real `POST`), NGX View Builder auto-merges the current page/sort/search state into the request body: ```json { "pagingParams": { "cnt": null, "orderClause": "lastName DESC", "pageSize": 25, "skipRows": 50, "totalCountUsed": false }, "params": [ ["tenantId", "42"] ], "extendedParams": [ { "paramName": "quickSearch", "paramValue": { "condition": "%-%", "value": "acme" } }, { "paramName": "status", "paramValue": { "condition": "=", "value": "active" } } ] } ``` `condition` values: `%-%` contains, `!%-%` not contains, `%-` starts with, `-%` ends with, `=`/`!=` equals/not equals, `>`/`>=`/`<`/`<=` for numbers and dates. Full contract, response shape, row-action/inline-edit context, and export behavior: [Table: server-side paging & filtering](https://ngxviewbuilder.io/developers/data-sources#table-server-side-paging-filtering-table-post). ### `tabs` / `tabsPro` Contains named tab sections. Child rows assigned via `column.tabRows`. ### `accordion` Collapsible sections with `label` per panel. ### `dialog` Modal element. Configure via `settings.dialog*` fields: | Setting | Notes | |---|---| | `settings.renderMode` | `"dialog"` | | `settings.dialogTitle` | Dialog title | | `settings.dialogDescription` | Subtitle | | `settings.dialogWidth` | Width | | `settings.dialogShowCloseButton` | boolean | | `settings.dialogPadding` | CSS padding | | `settings.dialogMaxWidth` | Max width | | `settings.dialogMaxHeight` | Max height | ### `richText` / `richTextViewer` Value shape: `string` (HTML). ### `messageCard` | Property | Type | Notes | |---|---|---| | `tone` | `string` | `primary` `success` `warning` `risk` `neutral` `info` | | `icon` | `string` | Icon name | | `text` | `string` | Card message text | ### `statsCard` | Property | Type | Notes | |---|---|---| | `value` | `string` | Main numeric/text value | | `trend` | `string` | Trend direction | | `trendValue` | `string` | Trend amount | ### `badge` | Property | Type | Notes | |---|---|---| | `text` | `string` | Badge content | | `tone` | `string` | `primary` `success` `warning` `risk` `neutral` `info` | ### `image` | Property | Type | Notes | |---|---|---| | `src` | `string` | Image URL or JEXL expression | | `alt` | `string` | Alt text | | `objectFit` | `string` | CSS object-fit | ### `icon` | Property | Type | Notes | |---|---|---| | `icon` | `string` | Icon name | | `size` | `string` | Icon size | | `tone` | `string` | Color tone | ### `divider` No additional properties beyond base. ### `spacer` | Property | Type | Notes | |---|---|---| | `height` | `string` | Spacer height | ### `pageTitle` | Property | Type | Notes | |---|---|---| | `title` | `string` | Heading text | | `subtitle` | `string` | Subheading text | ### `breadcrumbs` | Property | Type | Notes | |---|---|---| | `items` | `array` | Breadcrumb items | ### `iframe` | Property | Type | Notes | |---|---|---| | `src` | `string` | Source URL | | `height` | `string` | Frame height | | `scrolling` | `boolean` | | ### `slider` | Property | Type | Notes | |---|---|---| | `min` | `number` | | | `max` | `number` | | | `step` | `number` | | | `valueStorageType` | `string` | `number` or `string` | ### `numberStepper` | Property | Type | Notes | |---|---|---| | `step` | `number` | Increment/decrement step | | `min` | `number` | Min value | | `max` | `number` | Max value | Value shape: `number`. ### `timePicker` | Property | Type | Notes | |---|---|---| | `placeholder` | `string` | | | `minuteStep` | `number` | Minute increment shown in the picker | Value shape: time string. ### `signaturePad` | Property | Type | Notes | |---|---|---| | `canvasHeight` | `number` | Canvas height in px | | `clearLabel` | `string` | Clear button label | | `strokeColor` / `strokeWidth` / `backgroundColor` | `string` / `number` / `string` | Drawing style | | `exportFormat` | `string` | `png` or `jpeg` | Value shape: signature image data (per `exportFormat`), keyed under the element's `name`. ### `listBox` | Property | Type | Notes | |---|---|---| | `selectionMode` | `string` | `single` or `multiple` | | `dataSource` | `IElementDataSource` | Options from a datasource | | `options` | `array` | Static options | | `maxHeight` | `number` | List height in px | Value shape: single value (`selectionMode: "single"`) or array (`selectionMode: "multiple"`). ### `selectButton` | Property | Type | Notes | |---|---|---| | `multiple` | `boolean` | Allow selecting more than one option | | `allowEmpty` | `boolean` | Allow deselecting to no value | | `variant` | `string` | `buttons` or `segmented` | | `orientation` | `string` | `horizontal` or `vertical` | | `size` | `string` | `small` `normal` `large` | | `tone` | `string` | `primary` `neutral` `success` `info` `warning` `risk` | Value shape: single value, or array when `multiple` is `true`. ### `progressBar` | Property | Type | Notes | |---|---|---| | `min` / `max` | `number` | Value bounds | | `expression` | `string` | Computes the current value | | `dataSource` / `progressDataPath` / `progressValuePath` / `progressMaxPath` | - | Drive the value from a datasource | | `displayType` | `string` | `linear` or `circular` | | `valueLabelMode` | `string` | `percent` `value` `fraction` | Value shape: `number` (0 to `max`, default 100). --- ## `ISettings` fields Only include settings actually needed by the form. | Field | Type | Notes | |---|---|---| | `language` | `string` | Required. E.g. `"en"`, `"de"` | | `locale` | `string` | E.g. `"en-US"`, `"lt-LT"` | | `width` | `string` | Form width | | `widthUnit` | `"px" \| "%"` | | | `renderMode` | `"page" \| "dialog"` | | | `theme` | `"light" \| "dark"` | | | `pageNavigationMode` | `"default" \| "stepper"` | | | `allowStepWithoutValidation` | `boolean` | | | `pageNavigationPosition` | `"top" \| "bottom" \| "both" \| "none"` | | | `stepperPosition` | `"top" \| "bottom" \| "both" \| "none"` | | | `actionButtonsPosition` | `"top" \| "bottom" \| "both" \| "none"` | | | `showSubmitButton` | `boolean` | | | `showValidateButton` | `boolean` | | | `showValidationIssuesModal` | `boolean` | | | `elementSpacing` | `string` | Gap between rows | | `customCss` | `string` | Raw CSS injected | | `lazyElementRendering` | `boolean` | Lazy-render off-screen elements | --- ## `IDataSource` fields (top-level dataSources array) `{ name, title, type, params }`. `type` is one of `rest` `local` `route` `websocket`, and `params` is a plain object whose keys depend on that type. ```json { "dataSources": [ { "name": "ds1", "title": "List", "type": "rest", "params": { "url": "/api/items", "method": "GET" } }, { "name": "ds2", "title": "Save", "type": "rest", "params": { "url": "/api/items/{id}", "method": "PUT", "body": { "name": "{el1}" } } }, { "name": "ds3", "title": "Paged table", "type": "rest", "params": { "url": "/api/items/search", "method": "TABLE-POST" } }, { "name": "ds4", "title": "Static list", "type": "local", "params": { "localMode": "json", "dataJson": "[{\"id\":1,\"name\":\"A\"}]" } }, { "name": "ds5", "title": "From form data", "type": "local", "params": { "localMode": "dataPath", "dataPath": "el9.items" } }, { "name": "ds6", "title": "Route data", "type": "route", "params": { "routeDataKey": "record", "routeDataPath": "data" } }, { "name": "ds7", "title": "Live feed", "type": "websocket", "params": { "url": "wss://host/feed", "protocols": "", "message": "{}", "messagePath": "payload", "messageMode": "pushValuesInArray" } } ] } ``` | `type` | `params` keys | |---|---| | `rest` | `url` (required), `method` (`GET` `POST` `PUT` `PATCH` `DELETE`, or `TABLE-POST` for a lazy `table`), `body` (alias `payload`) | | `local` | `localMode` (`json` or `dataPath`), then `dataJson` (a JSON **string**) and `dataFunction`, or `dataPath` | | `route` | `routeDataKey`, `routeDataPath` | | `websocket` | `url`, `protocols`, `message`, `messagePath`, `messageMode` (`replaceCurrent` or `pushValuesInArray`) | URL and body templates use **single** braces: `{el1}`, `{row.id}`, `{__variables.tenantId}`. --- ## `IElementDataSource` (element-level dataSource field) ```json { "dataSource": { "name": "loadCities", "useFor": "option", "optionValue": "id", "optionLabel": "name", "refreshPaths": ["countryField"], "params": [{ "name": "country", "value": "{countryField}" }] } } ``` | Field | Type | Notes | |---|---|---| | `name` | `string` | References a top-level dataSource name | | `useFor` | `"option" \| "value"` | Whether datasource feeds options or element value | | `optionValue` | `string` | Property path for option value | | `optionLabel` | `string` | Property path for option label | | `filterOptionsBy` | `string` | JEXL expression using `item.*` to filter options | | `params` | `IDataSourceParamMapping[]` | `[{ "name": "key", "value": "{field}" }]` | | `refreshOnChange` | `boolean` | Re-fetch when any dependency changes | | `refreshPaths` | `string[]` | Re-fetch only when these fields change | --- ## Canonical type string list ``` text | textarea | number | slider | phoneInput | fileUpload | button | numberStepper | signaturePad select | multiSelect | radio | checkbox | singleCheckbox | toggleSwitch | toggleButton | autocomplete | selectButton | listBox datepicker | dateRange | timePicker panel | dynamicPanel | tabs | tabsPro | accordion | dialog | splitter | progressFlow | emptyBlock dynamicTable | table | listGrid | chart richText | richTextViewer | customHtml | htmlSnippet | image | video | iframe | avatar | icon | routerOutlet divider | spacer | breadcrumbs | pageTitle | badge | messageCard | statsCard | toast | progressBar page ``` Type strings are exact and case-sensitive. Never alter capitalization. --- ## AI: Common mistakes and anti-patterns Source: https://ngxviewbuilder.io/ai/common-mistakes # AI: Common mistakes and anti-patterns This page exists so that the agent does not keep repeating the same mistakes when generating NGX View Builder JSON. ## 1. Wrong element selection ### Mistake The user asks for a "dynamic table" and the agent generates `table`. ### What to do correctly - Use `dynamicTable` for editable rows with `add row` and `delete row`. - Use `table` for server-side data grid scenarios. ## 2. Self-reference expression ### Mistake ```json { "name": "lastName", "expression": "{firstName} == 'John' ? 'Doe' : {lastName}" } ``` ### Problem - the element references itself - the logic may not work or may behave unpredictably ### Correct ```json { "name": "lastName", "expression": "{firstName} == 'John' ? 'Doe' : ''", "logicExecutionMode": "onChange" } ``` ## 3. Mixing `pages` and `elements` ### Mistake - the agent places a full element object directly in `pages.rows.columns` ### Correct - `pages` holds only layout - `columns[*].elementRef` points to `elements` ## 4. Missing `page` entry in the `elements` map ### Mistake Present: ```json { "pages": [{ "name": "page1", "rows": [] }] } ``` but missing: ```json { "elements": { "page1": { "name": "page1", "type": "page", "label": "Page 1" } } } ``` ## 5. Generating non-existent properties ### Mistake - the agent invents custom properties that NGX View Builder does not have ### Rule - if you cannot find a property in the documentation or catalog, do not use it ## 6. Too much metadata ### Mistake - unnecessary `settings` or extra wrappers are added ### Rule - keep only properties that are actually used - if a feature is not active, it is better to omit its child properties ## 7. Wrong value shape Typical mistakes: - `dateRange` returns a string instead of `{ dateFrom, dateTo }` - `multiSelect` returns a single value instead of an array - `singleCheckbox` returns a string instead of `boolean` - `dynamicPanel` or `dynamicTable` does not return an array ## 8. Logic without `onChange` when recalculation is needed ### Symptom - the form renders, but the interdependency logic does not work as expected ### Why - the execution mode is missing, and the default is `onBlur` With `onBlur` the logic only re-runs once the field loses focus, so someone typing in the form sees nothing happen and reports the logic as broken. ### Rule - if one field's value must immediately affect another field, set `logicExecutionMode: "onChange"` - if validation has to follow the typing, set `validationExecutionMode: "onChange"` as well ## 8b. `hideIf` and other logic keys that do not exist ### Mistake ```json { "name": "companyCode", "hideIf": "{customerType} != 'company'" } ``` ### Problem There is no `hideIf`. The logic keys are exactly `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, `resetIf` and `expression`, and they are matched exactly, so `readOnlyIf` with a capital O misses too. This is the worst kind of mistake to debug, because nothing complains. The property is stored, the JSON reviews as correct, no dependency is ever registered for it, and the field simply stays visible forever. Deleting the element and adding the same rule by hand in the builder makes it work, which sends everyone hunting for a bug that is not there. ### Correct `visibleIf` states when the element **is** shown, so invert the condition instead of renaming the key. ```json { "name": "companyCode", "visibleIf": "{customerType} == 'company'", "logicExecutionMode": "onChange" } ``` ## 9. Overly aggressive rewrite of an existing form ### Mistake - the user asks to add one field, and the agent rebuilds the entire form from scratch ### Rule - in extension mode, change only what is relevant to the task - do not remove `dataSources`, `localization`, `settings`, or other sections without reason ## 10. Broken datasource references ### Mistake - an element references `dataSourceName` but no such datasource exists - a datasource exists but the item path or value/label mapping is wrong ### Rule - every datasource reference must point to a real, documented object ## 11. Using `name` instead of `key` in `table` columns ### Mistake ```json { "type": "table", "columnsConfig": [ { "name": "firstName", "label": "First Name", "type": "text" } ] } ``` ### Problem - `table` renders columns by `columnsConfig[*].key` - if only `name` is present, the column may not be rendered at all ### Correct ```json { "type": "table", "columnsConfig": [ { "key": "firstName", "label": "First Name", "type": "text", "showInTable": true } ] } ``` ## 12. Custom frontend instead of NGX View Builder ### Mistake - the agent starts suggesting `Angular` components, custom templates, or an API layer, even though an NGX View Builder form was requested ### Rule - the first choice is always the built-in NGX View Builder JSON model ## 13. Prose instead of JSON ### Mistake - the user asks to generate a form, and the agent writes an explanation followed by JSON ### Rule - in generation mode, return only JSON ## 14. Wrong element type casing ### Mistake ```json { "type": "datePicker" } { "type": "SingleCheckbox" } { "type": "DynamicTable" } { "type": "richtext" } { "type": "fileupload" } ``` ### Rule Type strings are **exact lowercase camelCase**. The canonical list: ``` text | textarea | number | slider | phoneInput | fileUpload | button | numberStepper | signaturePad select | multiSelect | radio | checkbox | singleCheckbox | toggleSwitch | toggleButton | autocomplete | selectButton | listBox datepicker | dateRange | timePicker panel | dynamicPanel | tabs | tabsPro | accordion | dialog | splitter | progressFlow | emptyBlock dynamicTable | table | listGrid | chart richText | richTextViewer | customHtml | htmlSnippet | image | video | iframe | avatar | icon | routerOutlet divider | spacer | breadcrumbs | pageTitle | badge | messageCard | statsCard | toast | progressBar page ``` Never invent capitalization variants. Copy from the list above. ## 15. Wrong validator field names ### Mistake ```json { "validators": [ { "type": "minLength", "expression": "{firstName}.length > 0", "message": "Required" } ] } ``` ### Problem - `expression` does not exist on `IValidator` - `visibleIf` does not exist on `IValidator` ### Correct validator schema ```json { "validators": [ { "type": "minLength", "value": 3, "message": "Minimum 3 characters", "applyIf": "{otherField} != ''" }, { "type": "custom", "condition": "dateDiffDays({startDate}, {endDate}) < 1", "message": "End date must be after start date" } ] } ``` Exact fields allowed on a validator: - `type`: validator type string (required) - `value`: threshold value when applicable (`number | string`) - `message`: error message shown to user - `condition`: JEXL expression, the *failing* check. The validator error is shown while it evaluates to `true` - `applyIf`: JEXL expression, the validator runs only when this is truthy **Never use `expression`, `visibleIf`, `disableIf`, or any other logic field on a validator object.** ## 16. Using logic field names that belong to elements on validators ### Mistake Copying `visibleIf`, `requireIf`, `readonlyIf`, `disableIf`, `expression`, `resetIf` from element-level logic into a validator object. Those fields do not exist on `IValidator`. ### Rule Element-level logic (`visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, `resetIf`, `expression`) belongs directly on the element, not inside the `validators` array. ```json { "name": "birthDate", "type": "datepicker", "requireIf": "{needsBirthDate} == true", "validators": [ { "type": "maxDate", "value": "today", "message": "Cannot be in the future" } ] } ``` ## 17. Wrapping logic fields in a `logic` object ### Mistake ```json { "name": "totalAmount", "type": "number", "logic": { "expression": "{price} * {qty}", "logicExecutionMode": "onChange" } } ``` ### Problem - There is no `logic` key in the NGX View Builder element schema. - The renderer ignores the `logic` wrapper, so the expression never runs. ### Correct Logic fields are **direct element-level properties**, never nested: ```json { "name": "totalAmount", "type": "number", "expression": "{price} * {qty}", "logicExecutionMode": "onChange" } ``` The same rule applies to `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, and `resetIf`. ## 18. Putting a container's children inside the element definition This is the single most damaging generation error. The panel renders, and it is **empty**. ### Mistake ```json { "elements": { "panelGeneral": { "name": "panelGeneral", "type": "panel", "label": "General", "rows": [ { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] } ] } } } ``` ### Problem - `IBaseElement` has no `rows` property. The array is ignored. - `firstName` and `lastName` are never placed in the layout tree, so they never render. - The same applies to invented keys such as `children`, `elements`, `items`, `content`, `fields`. ### Correct Children belong to the **column that references the container**, in `pages`: ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "panelGeneral", "rows": [ { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "type": "page", "label": "Page 1" }, "panelGeneral": { "name": "panelGeneral", "type": "panel", "label": "General" }, "firstName": { "name": "firstName", "type": "text", "label": "First name" }, "lastName": { "name": "lastName", "type": "text", "label": "Last name" } } } ``` `elements` is a **flat map**. It never nests. Full rules: [Layout model](https://ngxviewbuilder.io/ai/layout-model). ## 19. Widths written on columns ### Mistake ```json { "columns": [ { "elementRef": "firstName", "width": "50%", "mobileWidth": "100%" }, { "elementRef": "lastName", "width": "50%", "mobileWidth": "100%" } ] } ``` ### Problem - `IColumn` has exactly these keys: `elementRef`, `rows`, `tabRows`, `fragmentRef`, `fragmentBindings`, `fragmentMode`. Everything else is dropped. - The width was never applied, and the JSON is now noisier and harder to review. - Worse, it hides the real intent: two columns in one row **already** split evenly (`flex: 1 1 0`). ### Correct For an even split, write nothing: ```json { "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] } ``` For an uneven split, put `width` / `tabletWidth` / `mobileWidth` on the **element**: ```json { "postCode": { "name": "postCode", "label": "Post code", "type": "text", "width": "160px" } } ``` ## 20. Using `parentName` to declare structure ### Mistake ```json { "name": "firstName", "type": "text", "parentName": "panelGeneral" } ``` ### Problem `parentName` exists on `IBaseElement`, but it is runtime bookkeeping, not the structure declaration. Setting it does not place the element anywhere. ### Rule The layout tree in `pages` is the **only** source of parentage. ## 21. One page per visual section ### Mistake A screenshot shows four titled sections, and the agent emits four entries in `pages`. ### Problem A `page` is a **step or screen**, navigated by the pager or stepper. Four pages means a four-step wizard, not four sections stacked on one screen. ### Correct One page, four `panel` elements at the top level of its `rows`, each panel's `label` being the section heading. ## 22. Invented properties that look plausible Every one of these has been generated by an agent and none of them exists: | Invented | What is actually there | |---|---| | `panel.rows`, `panel.children`, `panel.items` | children go in `column.rows` | | `column.width`, `column.mobileWidth`, `column.span` | width goes on the element | | `singleCheckbox.checkedValue` / `uncheckedValue` | `singleCheckbox` stores `true`/`false`; the side text is `checkboxLabel` | | `phoneInput.maskType`, `phoneInput.inputMode`, `phoneInput.defaultValue` | `defaultCountryCode` (ISO code such as `"LT"`), `allowedCountryCodes` | | `text.maskType: "personalCodeLt"` / `"phoneLt"` | masks are exactly `none` `phoneIntl` `date` `time` `dateTime` `digits` `custom` | | `datepicker.format: "yyyy-MM-dd"` | uppercase tokens only: `YYYY-MM-DD`, `DD.MM.YYYY`, ... | | `element.logic { ... }` wrapper | logic fields sit directly on the element | | `validator.expression` | `validator.condition` | | `"type": "code"`, `"type": "dropdown"` | neither element type exists; use `textarea` and `select` | | `select.placeholder` for the closed-state text | that text is the UI translation `select.placeholder`; the property only sets the dropdown's search hint | | `dataSources[*].paramMap` | the field is `params` | | `"params": [["id", "{row.id}"]]` | `"params": [{ "name": "id", "value": "{row.id}" }]` | | `table.params: [{ "name", "value" }]` | table request params are `[{ "paramName", "paramValue" }]` | | `table.columnsConfig[*].name` | `key` | | `dynamicTable.columns[*].key` | `name` | ### Rule If a property is not in [the properties reference](https://ngxviewbuilder.io/ai/properties-reference), it does not exist. Omitting a nicety is always better than inventing a property: an invented property is silently ignored, so the form ships subtly broken with no error anywhere. ## Final anti-pattern checklist Before returning a response, the agent must verify: 1. Is the correct element chosen. 2. Are `pages` and `elements` not mixed up. 3. Do all `elementRef` values point to valid entries. 4. Is there no self-reference expression. 5. Is the value shape correct. 6. Are there no invented properties. 7. Do `table.columnsConfig[*]` entries use `key`, and `dynamicTable.columns[*]` entries use `name`. The two tables do not share a column shape. 8. Is the existing form context preserved. 9. Are logic fields (`expression`, `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, `resetIf`, `logicExecutionMode`) placed directly on the element, not inside a `logic` wrapper, and spelled exactly. No `hideIf`, no `readOnlyIf`. 9b. Does every element carrying logic or typing dependent validation set `logicExecutionMode` or `validationExecutionMode` to `onChange`, since both default to `onBlur`. 10. Does any object in `elements` contain `rows`, `columns`, `children` or `items` as a way of holding child elements. It must not. 11. Does any column object carry a key other than `elementRef`, `rows`, `tabRows`, `fragmentRef`, `fragmentBindings`, `fragmentMode`. 12. Are widths on elements rather than columns, and absent entirely wherever an even split is wanted. 13. Is the number of `pages` equal to the number of real steps, not the number of visual sections. --- ## AI: Verified example structures Source: https://ngxviewbuilder.io/ai/examples # AI: Verified example structures Every JSON block on this page is checked against the library source: element type strings against the element registry, property names against the builder property datasets, sub-object fields against the TypeScript interfaces. Copy the **shapes** from here; invent nothing that is not shown or listed in [the properties reference](https://ngxviewbuilder.io/ai/properties-reference). Names are deliberately generic (`el1`, `column1`, `ds1`) so the structure is what stands out, not the naming. ## How to read these examples 1. Layout is always `pages` → `rows` → `columns` → `elementRef`. Configuration is always the flat `elements` map. See [Layout model](https://ngxviewbuilder.io/ai/layout-model) first. 2. Anything a container holds is attached to the **column** that references it, never inside the element. 3. A property that does not appear on this page or in the properties reference does not exist. An invented property is dropped silently, so the view ships subtly broken with no error. --- ## 1. Skeleton and layout grammar One page, four bands: full width, even halves, uneven thirds, and a mobile-stacking trio. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "900px", "widthUnit": "px", "renderMode": "page", "elementSpacing": "12px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }] }, { "columns": [{ "elementRef": "el2" }, { "elementRef": "el3" }] }, { "columns": [{ "elementRef": "el4" }, { "elementRef": "el5" }] }, { "columns": [ { "elementRef": "el6" }, { "elementRef": "el7" }, { "elementRef": "el8" } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "Full width", "type": "text" }, "el2": { "name": "el2", "label": "Half", "type": "text" }, "el3": { "name": "el3", "label": "Half", "type": "text" }, "el4": { "name": "el4", "label": "Narrow", "type": "text", "width": "160px" }, "el5": { "name": "el5", "label": "Takes the rest", "type": "text" }, "el6": { "name": "el6", "label": "Third", "type": "text", "mobileWidth": "100%" }, "el7": { "name": "el7", "label": "Third", "type": "text", "mobileWidth": "100%" }, "el8": { "name": "el8", "label": "Third", "type": "text", "mobileWidth": "100%" } }, "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` What to take from it: - `el2` / `el3` split 50/50 with **no width property at all**. Columns with no width get `flex: 1 1 0`. - `el4` is fixed at `160px`, so `el5` absorbs the remainder. Width is on the element, never on the column. - `mobileWidth: "100%"` is what makes a row stack on phones. --- ## 2. Containers ### 2.1 Panels as titled sections ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "panel1", "rows": [ { "columns": [{ "elementRef": "el1" }, { "elementRef": "el2" }] }, { "columns": [{ "elementRef": "el3" }] } ] } ] }, { "columns": [ { "elementRef": "panel2", "rows": [{ "columns": [{ "elementRef": "el4" }, { "elementRef": "el5" }] }] } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page" }, "panel1": { "name": "panel1", "label": "Section one", "type": "panel", "showBorder": false, "panelPadding": "0px", "titleUnderline": true }, "panel2": { "name": "panel2", "label": "Section two", "type": "panel", "showBorder": true, "panelBorderWidth": "1px", "panelRadius": "8px", "panelPadding": "16px", "contentGap": "12px" }, "el1": { "name": "el1", "label": "Field 1", "type": "text" }, "el2": { "name": "el2", "label": "Field 2", "type": "text" }, "el3": { "name": "el3", "label": "Field 3", "type": "textarea", "rows": 3 }, "el4": { "name": "el4", "label": "Field 4", "type": "text" }, "el5": { "name": "el5", "label": "Field 5", "type": "text" } } } ``` ### 2.2 Tabs (`tabRows`, keyed by tab value) ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1", "tabRows": { "tab1": [{ "columns": [{ "elementRef": "el2" }, { "elementRef": "el3" }] }], "tab2": [{ "columns": [{ "elementRef": "el4" }] }] } } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page" }, "el1": { "name": "el1", "label": "Details", "type": "tabs", "tabsPosition": "top", "tabsVariant": "underline", "fullWidthTabs": false, "tabs": [ { "value": "tab1", "label": "General" }, { "value": "tab2", "label": "Address" } ] }, "el2": { "name": "el2", "label": "First name", "type": "text" }, "el3": { "name": "el3", "label": "Last name", "type": "text" }, "el4": { "name": "el4", "label": "Street", "type": "text" } } } ``` `tabsPro` is the same element with the same properties; its list property is `items` instead of `tabs`. ### 2.3 Accordion and splitter Both key their `tabRows` by the `value` of each entry in their own list property (`items` for accordion, `panels` for splitter). ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1", "tabRows": { "item1": [{ "columns": [{ "elementRef": "el3" }] }], "item2": [{ "columns": [{ "elementRef": "el4" }] }] } } ] }, { "columns": [ { "elementRef": "el2", "tabRows": { "left": [{ "columns": [{ "elementRef": "el5" }] }], "right": [{ "columns": [{ "elementRef": "el6" }] }] } } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page" }, "el1": { "name": "el1", "label": "Sections", "type": "accordion", "allowMultiple": false, "openFirst": true, "items": [ { "value": "item1", "label": "Contact" }, { "value": "item2", "label": "Billing" } ] }, "el2": { "name": "el2", "label": "Workspace", "type": "splitter", "orientation": "horizontal", "allowResize": true, "gutterSize": "6px", "minPanelSize": "160px", "showPanelHeaders": true, "panels": [ { "value": "left", "label": "Filters" }, { "value": "right", "label": "Results" } ] }, "el3": { "name": "el3", "label": "Email", "type": "text" }, "el4": { "name": "el4", "label": "IBAN", "type": "text" }, "el5": { "name": "el5", "label": "Search", "type": "text" }, "el6": { "name": "el6", "label": "Notes", "type": "textarea" } } } ``` ### 2.4 `emptyBlock` as a CSS grid `emptyBlock` is the unstyled layout box. Use it when a `panel` would add unwanted chrome, or when you need real grid/flex control. ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1", "rows": [ { "columns": [ { "elementRef": "el2" }, { "elementRef": "el3" }, { "elementRef": "el4" } ] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page" }, "el1": { "name": "el1", "type": "emptyBlock", "contentDisplay": "grid", "gridTemplateColumns": "repeat(3, minmax(0, 1fr))", "contentGap": "16px", "panelPadding": "16px", "panelBackgroundColor": "#f6f8fa", "panelRadius": "10px" }, "el2": { "name": "el2", "label": "Open", "type": "statsCard", "valueText": "128", "variant": "info" }, "el3": { "name": "el3", "label": "Closed", "type": "statsCard", "valueText": "47", "variant": "success" }, "el4": { "name": "el4", "label": "Overdue", "type": "statsCard", "valueText": "9", "variant": "risk" } } } ``` ### 2.5 In-view `dialog` element The `dialog` **element** is a modal that lives inside a page, opened by an action or `openIf`. It is not the same as `settings.renderMode: "dialog"`, which renders the whole view as a modal. ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }] }, { "columns": [ { "elementRef": "el2", "rows": [ { "columns": [{ "elementRef": "el3" }] }, { "columns": [{ "elementRef": "el4" }] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Page 1", "type": "page" }, "el1": { "name": "el1", "label": "", "type": "button", "text": "Edit record", "variant": "solid", "tone": "primary", "fitContent": true, "events": [ { "trigger": "click", "type": "dialog", "dialogName": "el2", "dialogOperation": "open" } ] }, "el2": { "name": "el2", "label": "Edit record", "type": "dialog", "showTriggerButton": false, "showCloseButton": true, "closeOnBackdrop": true, "dialogWidth": "560px" }, "el3": { "name": "el3", "label": "Title", "type": "text", "required": true }, "el4": { "name": "el4", "label": "", "type": "button", "text": "Save", "variant": "solid", "tone": "primary", "fitContent": true, "events": [ { "trigger": "click", "type": "dataSource", "dataSourceName": "ds1", "validateForm": true, "showToastAfter": true, "toastTitle": "Saved", "toastVariant": "success" }, { "trigger": "click", "type": "dialog", "dialogName": "el2", "dialogOperation": "close" } ] } } } ``` --- ## 3. Input elements Every property below is real. Notice what each element does **not** have: `phoneInput` has no mask, `singleCheckbox` has no `checkedValue`, `datepicker` formats use uppercase tokens. ```json { "elements": { "el1": { "name": "el1", "label": "Text", "type": "text", "placeholder": "Type here", "minlength": 2, "maxlength": 60, "showMaxLengthCounter": true, "inputMode": "text", "spellcheck": false, "autocomplete": "off", "required": true, "requiredMessage": "This field is required" }, "el2": { "name": "el2", "label": "Masked text", "type": "text", "maskType": "custom", "maskPattern": "AA-0000", "placeholder": "AB-1234" }, "el3": { "name": "el3", "label": "Textarea", "type": "textarea", "rows": 4, "maxlength": 500, "showMaxLengthCounter": true }, "el4": { "name": "el4", "label": "Number", "type": "number", "min": 0, "max": 1000000, "step": 0.01, "inputMode": "decimal", "valueStorageType": "number", "visualFormatEnabled": true, "visualFormatLocale": "lt-LT", "visualFormatUseGrouping": true, "visualFormatMinFractionDigits": 2, "visualFormatMaxFractionDigits": 2 }, "el5": { "name": "el5", "label": "Slider", "type": "slider", "min": 0, "max": 100, "step": 5, "showValueLabel": true, "defaultValue": 20 }, "el6": { "name": "el6", "label": "Stepper", "type": "numberStepper", "min": 1, "max": 20, "step": 1, "defaultValue": 1 }, "el7": { "name": "el7", "label": "Phone", "type": "phoneInput", "defaultCountryCode": "LT", "allowedCountryCodes": ["LT", "LV", "EE"], "placeholder": "600 00000" }, "el8": { "name": "el8", "label": "Date", "type": "datepicker", "placeholder": "Pick a date", "pickerMode": "date", "format": "YYYY-MM-DD", "minValue": "today", "validators": [{ "type": "maxDate", "value": "today", "message": "Cannot be in the future" }] }, "el9": { "name": "el9", "label": "Date and time", "type": "datepicker", "pickerMode": "datetime", "includeSeconds": false, "format": "YYYY-MM-DD HH:mm" }, "el10": { "name": "el10", "label": "Date range", "type": "dateRange", "format": "YYYY-MM-DD", "placeholder": "From - to" }, "el11": { "name": "el11", "label": "Time", "type": "timePicker", "minuteStep": 15 }, "el12": { "name": "el12", "label": "Attachments", "type": "fileUpload", "accept": ".pdf,image/*", "multiple": true, "maxFiles": 5, "maxFileSizeMb": 10, "showPreview": true, "dropzoneText": "Drop files here", "uploadDataSourceName": "ds1", "downloadDataSourceName": "ds2", "deleteDataSourceName": "ds3", "uploadFormFieldName": "file", "fileKeyField": "key", "fileNameField": "name", "fileTypeField": "contentType", "fileSizeField": "size" }, "el13": { "name": "el13", "label": "Signature", "type": "signaturePad", "canvasHeight": "180px", "strokeColor": "#111827", "strokeWidth": 2, "backgroundColor": "#ffffff", "exportFormat": "png", "clearLabel": "Clear" }, "el14": { "name": "el14", "label": "Rich text", "type": "richText", "toolbarTools": ["bold", "italic", "underline", "bulletList", "link"] } } } ``` Value shapes: `text` / `textarea` / `richText` store a string, `number` / `slider` / `numberStepper` a number, `dateRange` an object `{ dateFrom, dateTo }`, `fileUpload` the upload response object (or an array of them), `timePicker` a time string. --- ## 4. Choice elements ```json { "elements": { "el1": { "name": "el1", "label": "Select", "type": "select", "showSearch": true, "strictOptions": true, "defaultValue": "a", "options": [ { "value": "a", "label": "Option A" }, { "value": "b", "label": "Option B" }, { "value": "c", "label": "Option C" } ] }, "el2": { "name": "el2", "label": "Multi select", "type": "multiSelect", "showSearch": true, "strictOptions": true, "options": [ { "value": "a", "label": "Option A" }, { "value": "b", "label": "Option B" } ] }, "el3": { "name": "el3", "label": "Radio", "type": "radio", "showInline": true, "defaultValue": "y", "options": [ { "value": "y", "label": "Yes" }, { "value": "n", "label": "No" } ] }, "el4": { "name": "el4", "label": "Checkbox group", "type": "checkbox", "showInline": false, "options": [ { "value": "a", "label": "Option A" }, { "value": "b", "label": "Option B" } ] }, "el5": { "name": "el5", "label": "", "type": "singleCheckbox", "checkboxLabel": "I accept the terms", "defaultValue": false, "required": true, "requiredMessage": "You must accept the terms" }, "el6": { "name": "el6", "label": "Toggle", "type": "toggleSwitch", "trueLabel": "On", "falseLabel": "Off", "trueValue": true, "falseValue": false, "defaultValue": false }, "el7": { "name": "el7", "label": "Segmented", "type": "selectButton", "orientation": "horizontal", "multiple": false, "allowEmpty": false, "variant": "outline", "size": "normal", "showSelectedIcon": true, "options": [ { "value": "day", "label": "Day" }, { "value": "week", "label": "Week" }, { "value": "month", "label": "Month" } ] }, "el8": { "name": "el8", "label": "List box", "type": "listBox", "selectionMode": "multiple", "maxHeight": "220px", "options": [ { "value": "a", "label": "Option A" }, { "value": "b", "label": "Option B" }, { "value": "c", "label": "Option C" } ] }, "el9": { "name": "el9", "label": "Autocomplete", "type": "autocomplete", "placeholder": "Start typing", "minSearchLength": 2, "debounceMs": 300, "maxSuggestions": 20, "forceSelection": true, "lazyLoad": true, "queryContextKey": "query", "dataSource": { "name": "ds1", "useFor": "option", "optionValue": "id", "optionLabel": "name" } } } } ``` Value shapes: `select` / `radio` / `selectButton` / `autocomplete` store a single value; `multiSelect` / `checkbox` store an array; `listBox` stores a single value or an array depending on `selectionMode`; `singleCheckbox` / `toggleSwitch` store a boolean. --- ## 5. Content and feedback elements ```json { "elements": { "el1": { "name": "el1", "label": "", "type": "pageTitle", "title": "Customer records", "subtitle": "Everything the support desk needs", "level": 2, "align": "left", "showDivider": true }, "el2": { "name": "el2", "label": "", "type": "messageCard", "title": "Heads up", "descriptionText": "Changes are applied immediately.", "variant": "info", "showIcon": true, "dismissible": true, "initiallyVisible": true }, "el3": { "name": "el3", "label": "", "type": "statsCard", "title": "Open tickets", "valueText": "128", "variant": "info", "icon": "inbox", "trendDirection": "up", "trendText": "+12 this week" }, "el4": { "name": "el4", "label": "", "type": "badge", "text": "Beta", "variant": "warning", "pill": true, "size": "small" }, "el5": { "name": "el5", "label": "", "type": "progressBar", "min": 0, "max": 100, "displayType": "bar", "variant": "success", "showValueLabel": true, "valueLabelMode": "percent" }, "el6": { "name": "el6", "label": "", "type": "divider", "orientation": "horizontal", "thickness": "1px", "spacing": "16px" }, "el7": { "name": "el7", "label": "", "type": "spacer", "axis": "vertical", "size": "24px" }, "el8": { "name": "el8", "label": "", "type": "image", "src": "/assets/cover.png", "alt": "Cover", "fit": "cover", "aspectRatio": "16/9", "radius": "8px", "showPreview": true }, "el9": { "name": "el9", "label": "", "type": "customHtml", "htmlTemplate": "

Reference: {el1}

" }, "el10": { "name": "el10", "label": "", "type": "richTextViewer", "htmlSourcePath": "el11" }, "el11": { "name": "el11", "label": "", "type": "chart", "chartType": "bar", "chartHeight": "280px", "showLegend": true, "legendLayout": "horizontal", "labelKey": "label", "valueKey": "value", "chartDataPath": "stats.byMonth", "dataSource": { "name": "ds1", "useFor": "value" } } } } ``` --- ## 6. `table`: client-side data grid This is the shape to reach for when the endpoint returns every row in one response. The table sorts, searches, filters, paginates and exports entirely in the browser. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }] }, { "columns": [{ "elementRef": "el3" }] }, { "columns": [{ "elementRef": "el2" }] } ] } ], "elements": { "page1": { "name": "page1", "label": "Customers", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "", "type": "pageTitle", "title": "Customers", "level": 2 }, "el2": { "name": "el2", "label": "All customers", "type": "table", "dataSource": { "name": "ds1", "useFor": "value", "refreshOnChange": true, "refreshPaths": ["el3"] }, "tableItemsPath": "data.items", "lazyLoad": false, "pageSize": 25, "pageSizeOptions": "10,25,50,100", "paginator": true, "paginatorStyle": "default", "paginatorVariant": "full", "paginatorAlign": "end", "paginatorMaxButtons": 7, "showPageSizeSelector": true, "tableStyle": "striped", "stickyHeader": true, "rowHover": true, "responsiveMode": "auto", "flatHeaderSurface": false, "flatFooterSurface": false, "emptyMessage": "No customers found", "loadingLabel": "Loading...", "showHeaderControls": true, "showQuickSearch": true, "quickSearchPlaceholder": "Search customers", "quickSearchCondition": "%-%", "quickSearchCaseMode": "caseInsensitiveLatin", "searchDebounceMs": 300, "showDetailedSearch": true, "detailedSearchCaseMode": "caseInsensitiveLatin", "showExport": true, "exportFileName": "customers", "exportUseColumnPicker": true, "showColumnSettings": true, "columnSettingsMode": "localStorage", "columnSettingsStorageKey": "customersTableColumns", "columnSettingsDialogTitle": "Visible columns", "orderClause": "column2 ASC", "orderDirection": "asc", "columnsConfig": [ { "key": "column1", "label": "ID", "type": "number", "sortable": true, "width": "90px", "align": "right", "numberUseGrouping": false, "showInTable": true, "showInDetails": true }, { "key": "column2", "label": "Name", "type": "text", "sortable": true, "showInTable": true, "filterControlType": "text" }, { "key": "column3", "label": "Email", "type": "text", "sortable": true, "showInTable": true, "mobileLabel": "Mail" }, { "key": "column4", "label": "Balance", "type": "number", "sortable": true, "align": "right", "formatLocale": "lt-LT", "numberMinFractionDigits": 2, "numberMaxFractionDigits": 2, "numberUseGrouping": true, "showInTable": true, "filterControlType": "number" }, { "key": "column5", "label": "Created", "type": "dateTime", "sortable": true, "dateFormatPattern": "yyyy-MM-dd HH:mm", "dateIncludeSeconds": false, "showInTable": true, "filterControlType": "date" }, { "key": "column6", "label": "Country", "type": "text", "showInTable": true, "filterControlType": "select", "filterOptionsSourceType": "static", "filterOptions": [ { "value": "LT", "label": "Lithuania" }, { "value": "LV", "label": "Latvia" } ], "filterOptionValueKey": "value", "filterOptionLabelKey": "label" }, { "key": "column7", "label": "Internal note", "type": "text", "showInTable": false, "showInDetails": true, "detailLabel": "Note (internal)" } ] }, "el3": { "name": "el3", "label": "Segment", "type": "select", "options": [] } }, "dataSources": [ { "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers", "method": "GET" } } ], "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` Points that decide whether this works: - **`columnsConfig[*].key`, never `name`.** A column with only `name` does not render. - `type` for a column is one of `text` `date` `dateTime` `number` `element`. The renderer still understands the legacy `boolean` `html` `status` `toggleSwitch` `singleCheckbox` so old views keep working, but author new columns with `element` instead. - `tableItemsPath` is only needed when the rows are not the response root and not under `items` / `data` / `results` / `rows`. - `refreshPaths` on the element `dataSource` makes the table reload when another field changes. - `showInTable: false` + `showInDetails: true` keeps a field out of the grid but available in the details panel. ### 6.1 Row, header and selection actions ```json { "el2": { "name": "el2", "label": "All customers", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "columnsConfig": [ { "key": "column1", "label": "ID", "type": "number", "showInTable": true }, { "key": "column2", "label": "Name", "type": "text", "showInTable": true } ], "enableSelection": true, "selectionKey": "column1", "selectLabel": "Select", "selectAllLabel": "Select all", "selectionActionsLabel": "With selected", "autoSelectCondition": "row.column3 == 'active'", "selectionActions": [ { "label": "Archive selected", "type": "dataSource", "dataSourceName": "ds2", "buttonTone": "neutral", "confirmEnabled": true, "confirmTitle": "Archive", "confirmMessage": "Archive every selected record?", "reloadCurrentElementAfterSuccess": true } ], "headerActionsPosition": "end", "headerActionsDisplayMode": "buttons", "headerActions": [ { "label": "New customer", "icon": "add", "type": "navigate", "navigateTo": "/customers/new", "buttonVariant": "filled", "buttonTone": "primary" }, { "label": "Reload", "icon": "refresh", "type": "reloadElements", "reloadElementNames": ["el2"], "buttonVariant": "text" } ], "showActionsHeaderLabel": true, "actionsLabel": "Actions", "rowActionsDisplayMode": "dropdown", "rowActionsDropdownButtonStyle": "borderless", "rowActionVisibilityPath": "column4", "rowActions": [ { "label": "Open", "icon": "open_in_new", "type": "navigate", "navigateTo": "/customers/{row.column1}" }, { "label": "Deactivate", "icon": "block", "type": "dataSource", "dataSourceName": "ds3", "condition": "row.column3 == 'active'", "reloadCurrentElementAfterSuccess": true, "showToastAfter": true, "toastTitle": "Customer deactivated", "toastVariant": "success", "toastPosition": "bottom-right", "toastAutoHide": true, "toastAutoHideMs": 4000 }, { "label": "Delete", "icon": "delete", "type": "dataSource", "dataSourceName": "ds4", "buttonTone": "risk", "confirmEnabled": true, "confirmTitle": "Delete customer", "confirmMessage": "This cannot be undone. Continue?", "confirmConfirmLabel": "Delete", "confirmCancelLabel": "Cancel", "reloadCurrentElementAfterSuccess": true } ], "rowClickActions": [ { "type": "navigate", "navigateTo": "/customers/{row.column1}" } ] }, "dataSources": [ { "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers", "method": "GET" } }, { "name": "ds2", "title": "Archive", "type": "rest", "params": { "url": "/api/customers/archive", "method": "POST", "body": { "ids": "{selectedKeys}" } } }, { "name": "ds3", "title": "Deactivate", "type": "rest", "params": { "url": "/api/customers/{row.column1}", "method": "PUT", "body": { "active": false } } }, { "name": "ds4", "title": "Delete", "type": "rest", "params": { "url": "/api/customers/{row.column1}", "method": "DELETE" } } ] } ``` Context available to actions: | Action kind | Sees | | --- | --- | | `rowActions[*]`, `rowClickActions[*]` | `{row.*}` for the clicked row | | `selectionActions[*]` | `{selectedRows}`, `{selectedKeys}`, `{rows}`, `{items}` | | `headerActions[*]` | the surrounding form context, no row | | inline edit save | `{row.*}` plus `{changedValues}` (only the columns that changed) | `rowActions[*].condition` is evaluated per row against `row.*`. `rowActionVisibilityPath` names a row field that hides the whole action column for rows where it is falsy. ### 6.2 Element columns, status badges and templates A column of `type: "element"` renders a real builder element in every cell. `element` holds that element's own configuration, with the same property names it would have as a page element. ```json { "el2": { "name": "el2", "label": "Orders", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "columnsConfig": [ { "key": "column1", "label": "Order", "type": "text", "showInTable": true, "template": "#{row.column1}
{row.column2}" }, { "key": "column3", "label": "State", "type": "status", "statusShowIcon": true, "showInTable": true, "statusRules": [ { "condition": "value == 'new'", "label": "New", "tone": "info", "icon": "fiber_new", "rounded": true }, { "condition": "value == 'paid'", "label": "Paid", "tone": "success", "icon": "check_circle", "rounded": true }, { "condition": "value == 'late'", "label": "Late", "tone": "risk", "icon": "error", "rounded": true } ] }, { "key": "column4", "label": "Assignee", "type": "element", "elementType": "select", "showInTable": true, "controlEnabledIf": "row.column3 != 'paid'", "element": { "placeholder": "Unassigned", "strictOptions": true, "options": [ { "value": "u1", "label": "Anna" }, { "value": "u2", "label": "Ben" } ], "events": [ { "trigger": "change", "type": "dataSource", "dataSourceName": "ds2", "params": [ { "name": "id", "value": "{row.column1}" }, { "name": "assignee", "value": "{value}" } ] } ] } }, { "key": "column5", "label": "Progress", "type": "element", "elementType": "progressBar", "showInTable": true, "element": { "min": 0, "max": 100, "showValueLabel": true, "displayType": "bar" } }, { "key": "column6", "label": "Description", "type": "element", "elementType": "richTextViewer", "showInTable": false, "showInDetails": true, "element": {} }, { "key": "column7", "label": "", "type": "text", "showInTable": true, "cellActionsEnabled": true, "cellActionsDisplayMode": "iconButtons", "cellActions": [ { "label": "Copy", "icon": "content_copy", "type": "setValue", "setValueTargetPath": "el3", "setValueMode": "template", "setValueValue": "{row.column2}" } ] } ] } } ``` Rules for these: - **`statusRules[*].condition` reads the cell value as `value`**, not as the column key. `status == 'paid'` never matches; `value == 'paid'` does. `row.otherColumn` also works. - `statusRules[*].tone` is one of `neutral` `success` `warning` `risk` `primary` `danger` `info`. - Inside `template` and inside a hosted `element`, the row is reachable as `{row.*}`, a bare sibling key, `{index}` and `{value}`. - `visibleIf`, `controlEnabledIf` and `controlActiveIf` on a column are evaluated per row. - Action `params` are always `[{ "name": ..., "value": ... }]`. An array of pairs is dropped silently. ### 6.3 Server-side paging: `lazyLoad` and `TABLE-POST` With `lazyLoad: true` and the data source method set to the literal string `TABLE-POST`, the table merges paging, sorting and filter state into the request body for you. ```json { "elements": { "el2": { "name": "el2", "label": "Customers", "type": "table", "lazyLoad": true, "dataSource": { "name": "ds1", "useFor": "value" }, "tableItemsPath": "data.items", "tableTotalPath": "data.total", "params": [ { "paramName": "tenantId", "paramValue": "{__variables.tenantId}" }, { "paramName": "segment", "paramValue": "{el3}" } ], "pageSize": 50, "pageSizeOptions": "25,50,100", "orderClause": "column2 ASC", "showQuickSearch": true, "quickSearchParamName": "quickSearch", "quickSearchCondition": "%-%", "showDetailedSearch": true, "showExport": true, "exportAllPageSize": 10000, "virtualScroll": true, "virtualScrollRowHeight": 44, "virtualScrollViewportHeight": "600px", "virtualScrollBuffer": 10, "columnsConfig": [ { "key": "column1", "label": "ID", "type": "number", "sortable": true, "showInTable": true }, { "key": "column2", "label": "Name", "type": "text", "sortable": true, "showInTable": true, "filterControlType": "text" }, { "key": "column3", "label": "Active", "type": "element", "elementType": "badge", "showInTable": true, "filterControlType": "boolean", "filterToggleTrueValue": "true", "filterToggleFalseValue": "false", "element": {} } ] } }, "dataSources": [ { "name": "ds1", "title": "Customers (paged)", "type": "rest", "params": { "url": "/api/customers/search", "method": "TABLE-POST" } } ] } ``` The body your endpoint receives: ```json { "pagingParams": { "cnt": null, "orderClause": "column2 ASC", "pageSize": 50, "skipRows": 100, "totalCountUsed": false }, "params": [["tenantId", "42"], ["segment", "vip"]], "extendedParams": [ { "paramName": "quickSearch", "paramValue": { "condition": "%-%", "value": "acme", "upperLower": "caseInsensitiveLatin" } }, { "paramName": "column2", "paramValue": { "condition": "%-", "value": "Ac" } }, { "paramName": "column3", "paramValue": { "condition": "=", "value": true } } ] } ``` - On the wire `params` becomes `[name, value]` tuples, but **you author it as `[{ "paramName": ..., "paramValue": ... }]`** on the element. Up to five rows. - `condition` vocabulary: `%-%` contains, `!%-%` does not contain, `%-` starts with, `-%` ends with, `=`, `!=`, `>`, `>=`, `<`, `<=`. - Response: rows come from `tableItemsPath`, or `items` / `data` / `results` / `rows`. Total comes from `tableTotalPath`, or `total` / `totalCount` / `totalRecords` / `count` / `cnt` / `paging.*`. Without a recognizable total the pager looks stuck. - Export "all" re-issues the same request with `pagingParams.pageSize` set to `exportAllPageSize`, then builds the file in the browser. ### 6.4 Inline edit, expandable rows, details panel, saved filters ```json { "elements": { "el2": { "name": "el2", "label": "Orders", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "enableInlineEdit": true, "inlineEditStartMode": "doubleClick", "inlineEditSaveDataSourceName": "ds2", "inlineEditCancelOnReload": true, "expandableRows": true, "expandedDisplayMode": "inline", "showExpandChevron": true, "alwaysShowExpandChevron": false, "expandLabel": "Show items", "collapseLabel": "Hide items", "expandedEmptyMessage": "This order has no lines", "expandedDataSourceName": "ds3", "expandedDataSourceItemsPath": "data.lines", "expandedColumnsConfig": [ { "key": "column10", "label": "Product", "type": "text", "showInTable": true }, { "key": "column11", "label": "Qty", "type": "number", "align": "right", "showInTable": true }, { "key": "column12", "label": "Price", "type": "number", "align": "right", "numberMinFractionDigits": 2, "showInTable": true } ], "expandedRowActions": [ { "label": "Remove line", "icon": "delete", "type": "dataSource", "dataSourceName": "ds4", "buttonTone": "risk" } ], "rowClickOpensDetails": true, "detailsPanelTitle": "Order details", "enableSavedFilters": true, "savedFiltersMode": "dataSource", "savedFiltersDataSourceName": "ds5", "savedFiltersItemsPath": "data.items", "saveFilterDataSourceName": "ds6", "deleteFilterDataSourceName": "ds7", "savedFilterIdKey": "id", "savedFilterNameKey": "name", "savedFilterCodeKey": "code", "savedFilterDescriptionKey": "description", "savedFilterPayloadKey": "payload", "columnsConfig": [ { "key": "column1", "label": "Order", "type": "text", "showInTable": true }, { "key": "column2", "label": "Status", "type": "text", "showInTable": true, "editable": true, "editorType": "select", "editorOptions": [ { "value": "new", "label": "New" }, { "value": "paid", "label": "Paid" } ], "editorOptionValueKey": "value", "editorOptionLabelKey": "label" }, { "key": "column3", "label": "Note", "type": "text", "showInTable": true, "editable": true, "editorType": "textarea", "editorPlaceholder": "Internal note" } ] } } } ``` The inline-edit save data source receives `{row.*}` (the whole edited row) **and** `{changedValues}`, a flat object holding only the columns that actually changed, which is what a PATCH endpoint wants. ### 6.5 The table's live state: `__table.*` Every `table` continuously publishes its own state into the form data, so **any other element can read it without knowing how the table is fed**. This is the mechanism behind selection-driven toolbars, counters, master/detail screens and conditional buttons. Two roots exist. `__table..*` is that specific table. Bare `__table.*` is the last table the user touched, useful when there is only one. | Path | Holds | | --- | --- | | `__table..rows` | rows currently visible (the active page, after filtering) | | `__table..allRows` | every loaded row | | `__table..rowCount` | number of visible rows | | `__table..totalRecords` | total according to the server (lazy) or the loaded set | | `__table..page` | current page, 1-based | | `__table..size` | current page size | | `__table..sortField` | sorted column key, or `""` | | `__table..sortDirection` | `asc` or `desc` | | `__table..quickSearch` | current quick-search term | | `__table..detailedFilters` | active per-column filters (lazy) | | `__table..request` | the exact request payload last sent (lazy) | | `__table..selectedRows` | array of checkbox-selected row objects | | `__table..selectedKeys` | array of their `selectionKey` values | | `__table..selectedItems` | the same selection rendered through `selectionItemTemplate` | | `__table..selectedCount` | how many are selected | | `__table..selectedRow` | the single row the user is working on: first selected, else expanded/active, else last clicked | | `__table..selectedRowKey` | its key, or `null` | | `__table..selectedRowIndex` | its index, or `-1` | | `__table..activeRow` / `.activeRowKey` | the expanded / detail row | These are read in braces like any other path: `{__table.el2.selectedCount}`. ### 6.6 Selection-driven toolbar A counter, a bulk button that only enables with a selection, and a panel that mirrors the row being worked on. Nothing here is wired by hand; everything reads `__table.*`. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }, { "elementRef": "el2" }, { "elementRef": "el3" }] }, { "columns": [{ "elementRef": "el4" }] }, { "columns": [ { "elementRef": "el5", "rows": [ { "columns": [{ "elementRef": "el6" }, { "elementRef": "el7" }] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Orders", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "", "type": "badge", "text": "Selected: {__table.el4.selectedCount}", "variant": "info", "pill": true, "visibleIf": "{__table.el4.selectedCount} > 0" }, "el2": { "name": "el2", "label": "", "type": "button", "text": "Approve selected", "variant": "solid", "tone": "primary", "fitContent": true, "disableIf": "{__table.el4.selectedCount} == 0", "events": [ { "trigger": "click", "type": "dataSource", "dataSourceName": "ds2", "confirmEnabled": true, "confirmTitle": "Approve", "confirmMessage": "Approve every selected order?", "reloadElementNames": ["el4"], "showToastAfter": true, "toastTitle": "Approved", "toastVariant": "success" } ] }, "el3": { "name": "el3", "label": "", "type": "button", "text": "Clear search", "variant": "text", "tone": "neutral", "fitContent": true, "visibleIf": "notEmpty({__table.el4.quickSearch})", "events": [{ "trigger": "click", "type": "reloadElements", "reloadElementNames": ["el4"] }] }, "el4": { "name": "el4", "label": "Orders", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "enableSelection": true, "selectionKey": "column1", "selectionItemTemplate": "#{row.column1} ({row.column2})", "showQuickSearch": true, "pageSize": 25, "columnsConfig": [ { "key": "column1", "label": "Order", "type": "text", "showInTable": true, "sortable": true }, { "key": "column2", "label": "Customer", "type": "text", "showInTable": true }, { "key": "column3", "label": "Total", "type": "number", "align": "right", "numberMinFractionDigits": 2, "showInTable": true, "sortable": true } ] }, "el5": { "name": "el5", "label": "Current order", "type": "panel", "titleUnderline": true, "panelPadding": "0px", "showBorder": false, "visibleIf": "notEmpty({__table.el4.selectedRowKey})" }, "el6": { "name": "el6", "label": "Order", "type": "text", "readOnly": true, "expression": "{__table.el4.selectedRow}.column1", "logicExecutionMode": "onChange" }, "el7": { "name": "el7", "label": "Customer", "type": "text", "readOnly": true, "expression": "{__table.el4.selectedRow}.column2", "logicExecutionMode": "onChange" } }, "dataSources": [ { "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } }, { "name": "ds2", "title": "Approve selected", "type": "rest", "params": { "url": "/api/orders/approve", "method": "POST", "body": { "ids": "{__table.el4.selectedKeys}" } } } ], "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` `selectionKey` names the row field used as the identity of a selection; it defaults to `id`. `selectionItemTemplate` shapes each entry of `selectedItems`, which is what you send when the endpoint wants labels rather than raw rows. ### 6.7 Master and detail: one table drives another The detail table reloads whenever the master's selected row changes, because its data source declares that dependency in `refreshPaths`. ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }] }, { "columns": [{ "elementRef": "el2" }] } ] } ], "elements": { "page1": { "name": "page1", "label": "Orders", "type": "page" }, "el1": { "name": "el1", "label": "Orders", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "rowHover": true, "columnsConfig": [ { "key": "column1", "label": "Order", "type": "text", "showInTable": true }, { "key": "column2", "label": "Customer", "type": "text", "showInTable": true } ] }, "el2": { "name": "el2", "label": "Lines of the selected order", "type": "table", "emptyMessage": "Pick an order above", "visibleIf": "notEmpty({__table.el1.selectedRowKey})", "dataSource": { "name": "ds2", "useFor": "value", "params": [{ "name": "orderId", "value": "{__table.el1.selectedRowKey}" }], "refreshOnChange": true, "refreshPaths": ["__table.el1.selectedRowKey"] }, "columnsConfig": [ { "key": "column10", "label": "Product", "type": "text", "showInTable": true }, { "key": "column11", "label": "Qty", "type": "number", "align": "right", "showInTable": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } }, { "name": "ds2", "title": "Order lines", "type": "rest", "params": { "url": "/api/orders/{orderId}/lines", "method": "GET" } } ] } ``` Clicking a row is enough: `selectedRow` falls back to the last clicked row when there is no selection column. ### 6.8 A filter toolbar above the table Real fields drive the request. `params` decides what is sent, and each field change re-issues it. ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1", "rows": [ { "columns": [ { "elementRef": "el2" }, { "elementRef": "el3" }, { "elementRef": "el4" } ] } ] } ] }, { "columns": [{ "elementRef": "el5" }] } ] } ], "elements": { "page1": { "name": "page1", "label": "Reports", "type": "page" }, "el1": { "name": "el1", "label": "Filters", "type": "panel", "showBorder": true, "panelRadius": "8px", "panelPadding": "12px", "contentGap": "12px" }, "el2": { "name": "el2", "label": "Status", "type": "select", "strictOptions": true, "defaultValue": "", "options": [ { "value": "", "label": "All" }, { "value": "open", "label": "Open" }, { "value": "closed", "label": "Closed" } ] }, "el3": { "name": "el3", "label": "Period", "type": "dateRange", "format": "YYYY-MM-DD" }, "el4": { "name": "el4", "label": "Owner", "type": "text" }, "el5": { "name": "el5", "label": "Results", "type": "table", "lazyLoad": true, "dataSource": { "name": "ds1", "useFor": "value" }, "tableItemsPath": "data.items", "tableTotalPath": "data.total", "params": [ { "paramName": "status", "paramValue": "{el2}" }, { "paramName": "from", "paramValue": "{el3.dateFrom}" }, { "paramName": "to", "paramValue": "{el3.dateTo}" }, { "paramName": "owner", "paramValue": "{el4}" } ], "showQuickSearch": true, "pageSize": 50, "columnsConfig": [ { "key": "column1", "label": "Ref", "type": "text", "showInTable": true, "sortable": true }, { "key": "column2", "label": "Opened", "type": "date", "showInTable": true, "sortable": true }, { "key": "column3", "label": "Owner", "type": "text", "showInTable": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Report search", "type": "rest", "params": { "url": "/api/reports/search", "method": "TABLE-POST" } } ] } ``` A `dateRange` value is an object, so its parts are `{el3.dateFrom}` and `{el3.dateTo}`. ### 6.9 Column filter options loaded from a data source Per-column filters do not have to be hardcoded. A column can pull its own option list. ```json { "elements": { "el1": { "name": "el1", "label": "Tickets", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "showDetailedSearch": true, "columnsConfig": [ { "key": "column1", "label": "Ref", "type": "text", "showInTable": true }, { "key": "column2", "label": "Assignee", "type": "text", "showInTable": true, "filterControlType": "multiSelect", "filterOptionsSourceType": "datasource", "filterOptionsDataSourceName": "ds2", "filterOptionsDataSourceItemsPath": "data.items", "filterOptionValueKey": "id", "filterOptionLabelKey": "name", "filterMultiValueDelimiter": "," }, { "key": "column3", "label": "Archived", "type": "text", "showInTable": true, "filterControlType": "toggle", "filterToggleTrueValue": "true", "filterToggleFalseValue": "false" }, { "key": "column4", "label": "Priority", "type": "text", "showInTable": true, "filterControlType": "select", "filterOptionsSourceType": "static", "filterOptionValueKey": "value", "filterOptionLabelKey": "label", "filterOptions": [ { "value": "low", "label": "Low" }, { "value": "high", "label": "High" } ] } ] } }, "dataSources": [ { "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } }, { "name": "ds2", "title": "Users", "type": "rest", "params": { "url": "/api/users", "method": "GET" } } ] } ``` ### 6.10 Header dropdown Beside the header buttons a table can host one dropdown, either with static options or fed by a data source, whose selection runs actions. ```json { "elements": { "el1": { "name": "el1", "label": "Tickets", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "showHeaderControls": true, "headerActionsPosition": "end", "headerMenuEnabled": true, "headerMenuPlaceholder": "Switch queue", "headerMenuDataSourceName": "ds2", "headerMenuItemsPath": "data.items", "headerMenuLabelKey": "name", "headerMenuValueKey": "id", "headerMenuActions": [ { "trigger": "change", "type": "reloadElements", "reloadElementNames": ["el1"] } ], "columnsConfig": [ { "key": "column1", "label": "Ref", "type": "text", "showInTable": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } }, { "name": "ds2", "title": "Queues", "type": "rest", "params": { "url": "/api/queues", "method": "GET" } } ] } ``` For static options instead of a source, drop `headerMenuDataSourceName` / `headerMenuItemsPath` / `headerMenuLabelKey` / `headerMenuValueKey` and set `headerMenuOptions: [{ "value": "a", "label": "A" }]`. ### 6.11 Templates and mobile behaviour `headerTemplate` fills the centre of the header, `rowTemplate` replaces the whole row, `expandedTemplate` renders the expanded area. Row-level templates see `{row.*}`; all of them can read `{__table..*}`. ```json { "elements": { "el1": { "name": "el1", "label": "Tickets", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "headerTemplate": "Showing {__table.el1.rowCount} of {__table.el1.totalRecords}", "headerCenterElementsPosition": "center", "responsiveMode": "stack", "stickyHeader": true, "tableStyle": "grid", "expandableRows": true, "expandedDisplayMode": "inline", "expandedTemplate": "
{row.column2}

{row.column3}

", "columnsConfig": [ { "key": "column1", "label": "Ticket", "type": "text", "showInTable": true, "mobileLabel": "Ref", "template": "#{row.column1}" }, { "key": "column2", "label": "Subject", "type": "text", "showInTable": true }, { "key": "column3", "label": "Body", "type": "text", "showInTable": false, "showInDetails": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } } ] } ``` `responsiveMode: "stack"` turns each row into a stacked card on narrow screens, where `mobileLabel` becomes the visible field label. ### 6.12 Column settings stored on the server `columnSettingsMode: "localStorage"` needs only `columnSettingsStorageKey`. Per-user layouts kept server-side use the data-source mode instead. ```json { "elements": { "el1": { "name": "el1", "label": "Tickets", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "showColumnSettings": true, "columnSettingsDialogTitle": "Columns", "columnSettingsMode": "dataSource", "columnSettingsDataSourceName": "ds2", "columnSettingsDataSourcePath": "data.layout", "saveColumnSettingsDataSourceName": "ds3", "resetColumnSettingsDataSourceName": "ds4", "columnsConfig": [ { "key": "column1", "label": "Ref", "type": "text", "showInTable": true }, { "key": "column2", "label": "Subject", "type": "text", "showInTable": true }, { "key": "column3", "label": "Owner", "type": "text", "showInTable": false } ] } }, "dataSources": [ { "name": "ds1", "title": "Tickets", "type": "rest", "params": { "url": "/api/tickets", "method": "GET" } }, { "name": "ds2", "title": "Load layout", "type": "rest", "params": { "url": "/api/me/table-layout", "method": "GET" } }, { "name": "ds3", "title": "Save layout", "type": "rest", "params": { "url": "/api/me/table-layout", "method": "PUT" } }, { "name": "ds4", "title": "Reset layout", "type": "rest", "params": { "url": "/api/me/table-layout", "method": "DELETE" } } ] } ``` ### 6.13 Full CRUD loop: table, dialog editor, reload The pattern most admin screens need, end to end. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }] }, { "columns": [{ "elementRef": "el2" }] }, { "columns": [ { "elementRef": "el3", "rows": [ { "columns": [{ "elementRef": "el4" }, { "elementRef": "el5" }] }, { "columns": [{ "elementRef": "el6" }] }, { "columns": [ { "elementRef": "el7", "rows": [ { "columns": [{ "elementRef": "el8" }, { "elementRef": "el9" }] } ] } ] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Customers", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "", "type": "button", "text": "New customer", "icon": "add", "variant": "solid", "tone": "primary", "fitContent": true, "events": [ { "trigger": "click", "type": "setValue", "setValueTargetPath": "el4", "setValueMode": "template", "setValueValue": "" }, { "trigger": "click", "type": "setValue", "setValueTargetPath": "el5", "setValueMode": "template", "setValueValue": "" }, { "trigger": "click", "type": "setValue", "setValueTargetPath": "el6", "setValueMode": "template", "setValueValue": "" }, { "trigger": "click", "type": "dialog", "dialogName": "el3", "dialogOperation": "open" } ] }, "el2": { "name": "el2", "label": "Customers", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "selectionKey": "column1", "pageSize": 25, "showQuickSearch": true, "rowActionsDisplayMode": "iconButtons", "columnsConfig": [ { "key": "column1", "label": "ID", "type": "number", "showInTable": true, "width": "80px" }, { "key": "column2", "label": "Name", "type": "text", "showInTable": true, "sortable": true }, { "key": "column3", "label": "Email", "type": "text", "showInTable": true } ], "rowActions": [ { "label": "Edit", "icon": "edit", "type": "setValue", "setValueTargetPath": "el4", "setValueMode": "template", "setValueValue": "{row.column1}" }, { "label": "Edit", "icon": "edit", "hideText": true, "type": "setValue", "setValueTargetPath": "el5", "setValueMode": "template", "setValueValue": "{row.column2}" }, { "label": "Edit", "icon": "edit", "hideText": true, "type": "setValue", "setValueTargetPath": "el6", "setValueMode": "template", "setValueValue": "{row.column3}" }, { "label": "Edit", "icon": "edit", "hideText": true, "type": "dialog", "dialogName": "el3", "dialogOperation": "open" }, { "label": "Delete", "icon": "delete", "type": "dataSource", "dataSourceName": "ds3", "buttonTone": "risk", "confirmEnabled": true, "confirmTitle": "Delete customer", "confirmMessage": "This cannot be undone.", "reloadCurrentElementAfterSuccess": true } ] }, "el3": { "name": "el3", "label": "Customer", "type": "dialog", "showTriggerButton": false, "showCloseButton": true, "dialogWidth": "520px" }, "el4": { "name": "el4", "label": "ID", "type": "text", "readOnly": true }, "el5": { "name": "el5", "label": "Name", "type": "text", "required": true }, "el6": { "name": "el6", "label": "Email", "type": "text", "inputMode": "email", "required": true }, "el7": { "name": "el7", "label": "", "type": "panel", "showBorder": false, "panelPadding": "0px", "contentJustify": "flex-end", "contentGap": "8px" }, "el8": { "name": "el8", "label": "", "type": "button", "text": "Cancel", "variant": "outline", "tone": "neutral", "fitContent": true, "events": [{ "trigger": "click", "type": "dialog", "dialogName": "el3", "dialogOperation": "close" }] }, "el9": { "name": "el9", "label": "", "type": "button", "text": "Save", "variant": "solid", "tone": "primary", "fitContent": true, "events": [ { "trigger": "click", "type": "dataSource", "dataSourceName": "ds2", "validateForm": true, "reloadElementNames": ["el2"], "showToastAfter": true, "toastTitle": "Saved", "toastVariant": "success" }, { "trigger": "click", "type": "dialog", "dialogName": "el3", "dialogOperation": "close" } ] } }, "dataSources": [ { "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers", "method": "GET" } }, { "name": "ds2", "title": "Save customer", "type": "rest", "params": { "url": "/api/customers/{el4}", "method": "PUT", "body": { "name": "{el5}", "email": "{el6}" } } }, { "name": "ds3", "title": "Delete customer", "type": "rest", "params": { "url": "/api/customers/{row.column1}", "method": "DELETE" } } ], "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` One action carries one `type`, so filling three fields and opening the dialog is four actions on the same row-action button. They run in order, and only the first needs a visible label; give the rest `hideText: true` so the toolbar stays clean. ### 6.14 Dashboard: one table feeding KPI cards and a chart The table is the single source of data; the cards and the chart read its published state. No second request is issued: a `local` source in `dataPath` mode simply points at `__table.el3.allRows`. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1180px", "widthUnit": "px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1" }, { "elementRef": "el2" } ] }, { "columns": [{ "elementRef": "el5" }] }, { "columns": [{ "elementRef": "el4" }] }, { "columns": [{ "elementRef": "el3" }] } ] } ], "elements": { "page1": { "name": "page1", "label": "Sales", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "", "type": "statsCard", "title": "Orders", "valueText": "{__table.el3.totalRecords}", "variant": "info", "icon": "receipt_long" }, "el2": { "name": "el2", "label": "", "type": "statsCard", "title": "Revenue", "valueText": "{el5}", "variant": "success", "icon": "payments" }, "el4": { "name": "el4", "label": "Revenue by month", "type": "chart", "chartType": "bar", "chartHeight": "260px", "showLegend": false, "labelKey": "column2", "valueKey": "column3", "dataSource": { "name": "ds2", "useFor": "value", "refreshOnChange": true, "refreshPaths": ["__table.el3.allRows"] } }, "el3": { "name": "el3", "label": "Orders", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "pageSize": 25, "showQuickSearch": true, "columnsConfig": [ { "key": "column1", "label": "Order", "type": "text", "showInTable": true }, { "key": "column2", "label": "Month", "type": "text", "showInTable": true }, { "key": "column3", "label": "Total", "type": "number", "align": "right", "numberMinFractionDigits": 2, "showInTable": true } ] }, "el5": { "name": "el5", "label": "Revenue", "type": "number", "hidden": true, "readOnly": true, "expression": "sumArray({__table.el3.allRows}, column3)", "logicExecutionMode": "onChange" } }, "dataSources": [ { "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } }, { "name": "ds2", "title": "Orders already loaded by the table", "type": "local", "params": { "localMode": "dataPath", "dataPath": "__table.el3.allRows" } } ], "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` `statsCard.title`, `valueText` and `trendText`, and `badge.text`, all interpolate `{...}` tokens, so a card can display live table state directly. A hidden `number` element is the usual place to park an aggregate that several other elements want to reuse. **`hidden: true` still needs a place in the layout tree.** `hidden` only suppresses the rendering; an element that is absent from `pages` is never instantiated at all, so its `expression` never runs and everything reading it stays empty. Give the helper element its own row and hide it there. ### 6.15 Aggregates under a table `allRows` is the whole loaded set, `rows` is only what the current page shows after filtering. Pick deliberately: a "total" that changes when the user pages is almost always a bug. ```json { "elements": { "el1": { "name": "el1", "label": "Invoices", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "columnsConfig": [ { "key": "column1", "label": "Invoice", "type": "text", "showInTable": true }, { "key": "column2", "label": "Status", "type": "text", "showInTable": true }, { "key": "column3", "label": "Amount", "type": "number", "align": "right", "showInTable": true } ] }, "el2": { "name": "el2", "label": "Rows loaded", "type": "number", "readOnly": true, "expression": "len({__table.el1.allRows})", "logicExecutionMode": "onChange" }, "el3": { "name": "el3", "label": "Total amount", "type": "number", "readOnly": true, "expression": "sumArray({__table.el1.allRows}, column3)", "logicExecutionMode": "onChange" }, "el4": { "name": "el4", "label": "Average amount", "type": "number", "readOnly": true, "expression": "avgArray({__table.el1.allRows}, column3)", "logicExecutionMode": "onChange" }, "el5": { "name": "el5", "label": "Unpaid", "type": "number", "readOnly": true, "expression": "countInArray({__table.el1.allRows}, column2 == \"unpaid\")", "logicExecutionMode": "onChange" }, "el6": { "name": "el6", "label": "Unpaid total", "type": "number", "readOnly": true, "expression": "sumArray(filterArray({__table.el1.allRows}, column2 == \"unpaid\"), column3)", "logicExecutionMode": "onChange" }, "el7": { "name": "el7", "label": "Largest invoice", "type": "text", "readOnly": true, "expression": "getFirst(filterArray({__table.el1.allRows}, column3 > 1000), column1)", "logicExecutionMode": "onChange" } }, "dataSources": [ { "name": "ds1", "title": "Invoices", "type": "rest", "params": { "url": "/api/invoices", "method": "GET" } } ] } ``` Inside a collection function the row fields are written **bare and unquoted** (`column2 == "unpaid"`); everything outside the collection keeps its braces. ### 6.16 Details side panel instead of an inline expand The same expand machinery renders in a drawer when `expandedDisplayMode` is `sidePanel`. `rowClickOpensDetails` means the whole row opens it, so no chevron column is needed. ```json { "elements": { "el1": { "name": "el1", "label": "Applications", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "expandableRows": true, "expandedDisplayMode": "sidePanel", "detailsPanelTitle": "Application details", "rowClickOpensDetails": true, "showExpandChevron": false, "rowHover": true, "rowExpandActions": [ { "type": "dataSource", "dataSourceName": "ds2", "params": [{ "name": "id", "value": "{row.column1}" }] } ], "columnsConfig": [ { "key": "column1", "label": "Ref", "type": "text", "showInTable": true }, { "key": "column2", "label": "Applicant", "type": "text", "showInTable": true }, { "key": "column3", "label": "Submitted", "type": "date", "showInTable": true }, { "key": "column4", "label": "Motivation", "type": "text", "showInTable": false, "showInDetails": true, "detailLabel": "Why they applied" }, { "key": "column5", "label": "Reviewer notes", "type": "text", "showInTable": false, "showInDetails": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Applications", "type": "rest", "params": { "url": "/api/applications", "method": "GET" } }, { "name": "ds2", "title": "Mark as viewed", "type": "rest", "params": { "url": "/api/applications/{id}/viewed", "method": "POST" } } ] } ``` `rowExpandActions` fire when a row is expanded, which is where "mark as read" or "load the heavy part now" belongs. Columns with `showInTable: false` and `showInDetails: true` are exactly what fills the panel. ### 6.17 Row action that downloads a file `responseMode: "download"` turns the response into a file instead of data. ```json { "elements": { "el1": { "name": "el1", "label": "Invoices", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "rowActionsDisplayMode": "iconButtons", "columnsConfig": [ { "key": "column1", "label": "Invoice", "type": "text", "showInTable": true }, { "key": "column2", "label": "Issued", "type": "date", "showInTable": true } ], "rowActions": [ { "label": "Download PDF", "icon": "download", "type": "dataSource", "dataSourceName": "ds2", "responseMode": "download", "responseFileName": "invoice-{row.column1}.pdf" }, { "label": "Email a copy", "icon": "mail", "type": "dataSource", "dataSourceName": "ds3", "condition": "notEmpty({row.column3})", "showToastAfter": true, "toastTitle": "Sent", "toastVariant": "success" } ] } }, "dataSources": [ { "name": "ds1", "title": "Invoices", "type": "rest", "params": { "url": "/api/invoices", "method": "GET" } }, { "name": "ds2", "title": "Invoice PDF", "type": "rest", "params": { "url": "/api/invoices/{row.column1}/pdf", "method": "GET" } }, { "name": "ds3", "title": "Email invoice", "type": "rest", "params": { "url": "/api/invoices/{row.column1}/email", "method": "POST" } } ] } ``` CSV/Excel/PDF export of the grid itself is different: it is `showExport` and happens entirely in the browser, with no endpoint to implement. ### 6.18 Export configuration ```json { "elements": { "el1": { "name": "el1", "label": "Customers", "type": "table", "lazyLoad": true, "dataSource": { "name": "ds1", "useFor": "value" }, "tableItemsPath": "data.items", "tableTotalPath": "data.total", "pageSize": 50, "showExport": true, "exportFileName": "customers-2026", "exportUseColumnPicker": true, "exportAllPageSize": 20000, "columnsConfig": [ { "key": "column1", "label": "ID", "type": "number", "showInTable": true }, { "key": "column2", "label": "Name", "type": "text", "showInTable": true }, { "key": "column3", "label": "Created", "type": "dateTime", "dateFormatPattern": "yyyy-MM-dd", "showInTable": true }, { "key": "column4", "label": "Internal score", "type": "number", "showInTable": false, "showInDetails": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Customers", "type": "rest", "params": { "url": "/api/customers/search", "method": "TABLE-POST" } } ] } ``` With `exportUseColumnPicker` the user chooses which columns land in the file. "Export all" re-issues the same table request with `pagingParams.pageSize` set to `exportAllPageSize`, so that number is a real memory decision, not decoration. ### 6.19 A live table over a websocket ```json { "elements": { "el1": { "name": "el1", "label": "Live events", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "pageSize": 50, "stickyHeader": true, "virtualScroll": true, "virtualScrollRowHeight": 40, "virtualScrollViewportHeight": "520px", "orderClause": "column1 DESC", "columnsConfig": [ { "key": "column1", "label": "Time", "type": "dateTime", "dateIncludeSeconds": true, "showInTable": true, "sortable": true }, { "key": "column2", "label": "Level", "type": "status", "showInTable": true, "statusShowIcon": true, "statusRules": [ { "condition": "value == 'error'", "label": "Error", "tone": "risk", "icon": "error", "rounded": true }, { "condition": "value == 'warn'", "label": "Warning", "tone": "warning", "icon": "warning", "rounded": true }, { "condition": "value == 'info'", "label": "Info", "tone": "info", "rounded": true } ] }, { "key": "column3", "label": "Message", "type": "text", "showInTable": true } ] } }, "dataSources": [ { "name": "ds1", "title": "Event stream", "type": "websocket", "params": { "url": "wss://example.test/events", "protocols": "", "message": "{\"subscribe\":\"events\"}", "messagePath": "payload", "messageMode": "pushValuesInArray" } } ] } ``` `messageMode: "pushValuesInArray"` appends each message to the accumulated array, which is what a log-style table wants; `replaceCurrent` swaps the whole set instead. The buffer is capped at 500 entries, so a view left open overnight cannot grow without end. `virtualScroll` keeps a long list cheap to render. ### 6.20 Tables nested in containers A table is a leaf element, so it sits in a column like any other. Inside `tabs` that column lives in `tabRows`; inside a `dynamicPanel` each entry renders its own copy of the table. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1100px", "widthUnit": "px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1", "tabRows": { "active": [{ "columns": [{ "elementRef": "el2" }] }], "archived": [{ "columns": [{ "elementRef": "el3" }] }] } } ] }, { "columns": [ { "elementRef": "el4", "rows": [ { "columns": [{ "elementRef": "el5" }] }, { "columns": [{ "elementRef": "el6" }] } ] } ] } ] } ], "elements": { "page1": { "name": "page1", "label": "Projects", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "Projects", "type": "tabs", "tabsPosition": "top", "tabs": [ { "value": "active", "label": "Active" }, { "value": "archived", "label": "Archived" } ] }, "el2": { "name": "el2", "label": "Active projects", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "columnsConfig": [ { "key": "column1", "label": "Project", "type": "text", "showInTable": true }, { "key": "column2", "label": "Owner", "type": "text", "showInTable": true } ] }, "el3": { "name": "el3", "label": "Archived projects", "type": "table", "dataSource": { "name": "ds2", "useFor": "value" }, "columnsConfig": [ { "key": "column1", "label": "Project", "type": "text", "showInTable": true }, { "key": "column3", "label": "Archived", "type": "date", "showInTable": true } ] }, "el4": { "name": "el4", "label": "Teams", "type": "dynamicPanel", "addRowButtonText": "Add team", "panelPadding": "16px", "panelBorderWidth": "1px", "panelRadius": "8px" }, "el5": { "name": "el5", "label": "Team name", "type": "text", "required": true }, "el6": { "name": "el6", "label": "Members", "type": "dynamicTable", "addRowButtonText": "Add member", "columns": [ { "name": "column1", "label": "Name", "type": "text" }, { "name": "column2", "label": "Role", "type": "select", "options": [ { "value": "dev", "label": "Developer" }, { "value": "qa", "label": "QA" } ] } ] } }, "dataSources": [ { "name": "ds1", "title": "Active", "type": "rest", "params": { "url": "/api/projects?state=active", "method": "GET" } }, { "name": "ds2", "title": "Archived", "type": "rest", "params": { "url": "/api/projects?state=archived", "method": "GET" } } ], "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` Note which repeating element goes where: a **grid of editable cells** inside a repeating block is `dynamicTable`; a **read-only view of server data** would be `table`. A `table` inside a `dynamicPanel` publishes its state under one shared `__table.` key, so prefer `dynamicTable` when each entry needs its own independent state. ### 6.21 A localized table Column labels, action labels and messages all go through `localization.texts` when a `translate()` call or a translated key is used. The simplest route is to author the labels in the default language and let the host supply the rest. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1100px", "widthUnit": "px" }, "pages": [{ "name": "page1", "rows": [{ "columns": [{ "elementRef": "el1" }] }] }], "elements": { "page1": { "name": "page1", "label": "Orders", "type": "page", "hideHeader": true }, "el1": { "name": "el1", "label": "Orders", "type": "table", "dataSource": { "name": "ds1", "useFor": "value" }, "emptyMessage": "No records found", "loadingLabel": "Kraunama...", "quickSearchPlaceholder": "Search", "actionsLabel": "Veiksmai", "showQuickSearch": true, "columnsConfig": [ { "key": "column1", "label": "Numeris", "type": "text", "showInTable": true }, { "key": "column2", "label": "Suma", "type": "number", "align": "right", "formatLocale": "lt-LT", "numberMinFractionDigits": 2, "numberUseGrouping": true, "showInTable": true }, { "key": "column3", "label": "Status", "type": "element", "elementType": "customHtml", "showInTable": true, "element": { "htmlTemplate": "{translate({row.column3})}" } } ] } }, "dataSources": [ { "name": "ds1", "title": "Orders", "type": "rest", "params": { "url": "/api/orders", "method": "GET" } } ], "localization": { "defaultLanguage": "en", "languages": ["en", "de"], "texts": { "de": { "new": "Neu", "paid": "Bezahlt", "late": "Überfällig" }, "en": { "new": "New", "paid": "Paid", "late": "Late" } } } } ``` `translate(value)` looks the raw value up in `localization.texts[]`, which is how a raw server code such as `paid` becomes a readable label without a lookup table in the view. ### 6.22 Choosing the right table | The screen needs | Use | | --- | --- | | Users type into every row, add and delete rows | `dynamicTable` | | A read-only or lightly interactive grid over server data | `table` | | Server owns paging, sorting and filtering | `table` with `lazyLoad: true` and `TABLE-POST` | | Cards instead of rows | `listGrid` | | A repeating block of fields rather than a grid of cells | `dynamicPanel` | `table` never uses `column.rows`; its cells are `columnsConfig`, keyed by `key`. `dynamicTable` never uses `columnsConfig`; its cells are `columns`, keyed by `name`. ### 6.23 `table` property map Grouped so nothing has to be guessed. | Group | Properties | | --- | --- | | Data | `dataSource` `tableItemsPath` `tableTotalPath` `lazyLoad` `params` `orderClause` `orderDirection` | | Columns | `columnsConfig` `showColumnSettings` `columnSettingsMode` `columnSettingsStorageKey` `columnSettingsDialogTitle` `columnSettingsDataSourceName` `columnSettingsDataSourcePath` `saveColumnSettingsDataSourceName` `resetColumnSettingsDataSourceName` | | Paging | `pageSize` `pageSizeOptions` `paginator` `paginatorStyle` `paginatorVariant` `paginatorAlign` `paginatorMaxButtons` `showPageSizeSelector` `prevLabel` `nextLabel` | | Search & filters | `showQuickSearch` `quickSearchPlaceholder` `quickSearchParamName` `quickSearchCondition` `quickSearchCaseMode` `searchDebounceMs` `showDetailedSearch` `detailedSearchCaseMode` `enableSavedFilters` `savedFiltersMode` `savedFiltersDataSourceName` `savedFiltersItemsPath` `saveFilterDataSourceName` `deleteFilterDataSourceName` `savedFilterIdKey` `savedFilterNameKey` `savedFilterCodeKey` `savedFilterDescriptionKey` `savedFilterPayloadKey` | | Actions | `rowActions` `rowActionsDisplayMode` `rowActionsDropdownButtonStyle` `rowActionVisibilityPath` `rowClickActions` `rowExpandActions` `headerActions` `headerActionsPosition` `headerActionsDisplayMode` `headerMenuEnabled` `headerMenuActions` `headerMenuOptions` `headerMenuPlaceholder` `headerMenuDataSourceName` `headerMenuDataSourceItemsPath` `headerMenuItemsPath` `headerMenuLabelKey` `headerMenuValueKey` `actionsLabel` `showActionsHeaderLabel` | | Selection | `enableSelection` `selectionKey` `selectionActions` `selectionActionsLabel` `selectionItemTemplate` `selectLabel` `selectAllLabel` `selectRowLabel` `autoSelectCondition` | | Inline edit | `enableInlineEdit` `inlineEditStartMode` `inlineEditSaveDataSourceName` `inlineEditCancelOnReload` | | Expand & details | `expandableRows` `expandedDisplayMode` `expandedColumnsConfig` `expandedDataSourceName` `expandedDataSourceItemsPath` `expandedItemsPath` `expandedTemplate` `expandedEmptyMessage` `expandedRowActions` `showExpandChevron` `alwaysShowExpandChevron` `expandLabel` `collapseLabel` `rowClickOpensDetails` `detailsPanelTitle` | | Export | `showExport` `exportFileName` `exportUseColumnPicker` `exportAllPageSize` | | Appearance | `tableStyle` `stickyHeader` `rowHover` `responsiveMode` `flatHeaderSurface` `flatFooterSurface` `showHeaderControls` `headerCenterElementsPosition` `headerTemplate` `rowTemplate` `rowTemplateName` `emptyMessage` `loadingLabel` `width` `tabletWidth` `mobileWidth` `hidden` `visibleIf` | | Virtual scroll | `virtualScroll` `virtualScrollRowHeight` `virtualScrollViewportHeight` `virtualScrollBuffer` | Column (`columnsConfig[*]`) properties: | Group | Properties | | --- | --- | | Identity | `key` (required) `label` `showHeaderLabel` `mobileLabel` `type` `showInTable` `showInDetails` `detailLabel` `sortable` `width` `align` `visibleIf` | | Formatting | `formatLocale` `dateFormatPattern` `dateIncludeSeconds` `numberMinFractionDigits` `numberMaxFractionDigits` `numberUseGrouping` | | Element cells | `elementType` `element` `controlActiveIf` `controlEnabledIf` | | Status cells | `statusRules` `statusShowIcon` | | Templates | `template` `templateName` `templateFieldMap` | | Filters | `filterControlType` `filterOptionsSourceType` `filterOptions` `filterOptionsJson` `filterOptionsPath` `filterOptionsDataSourceName` `filterOptionsDataSourceItemsPath` `filterOptionLabelKey` `filterOptionValueKey` `filterMultiValueDelimiter` `filterToggleTrueValue` `filterToggleFalseValue` | | Inline edit | `editable` `editorType` `editorPlaceholder` `editorOptions` `editorOptionLabelKey` `editorOptionValueKey` | | Cell actions | `cellActionsEnabled` `cellActions` `cellActionsDisplayMode` `cellActionsPlaceholder` | Enumerations, exact values: | Property | Values | | --- | --- | | `columnsConfig[*].type` | `text` `date` `dateTime` `number` `element` (legacy, still rendered: `boolean` `html` `status` `toggleSwitch` `singleCheckbox`) | | `columnsConfig[*].align` | `left` `center` `right` | | `columnsConfig[*].filterControlType` | `auto` `text` `number` `date` `dateTime` `boolean` `select` `multiSelect` `toggle` | | `columnsConfig[*].editorType` | `auto` `text` `number` `date` `textarea` `checkbox` `toggle` `select` | | `columnsConfig[*].filterOptionsSourceType` | `static` `datasource` | | `statusRules[*].tone` | `neutral` `success` `warning` `risk` `primary` `danger` `info` | | `tableStyle` | `default` `grid` `striped` | | `responsiveMode` | `auto` `stack` | | `rowActionsDisplayMode` | `dropdown` `iconButtons` `buttons` | | `rowActionsDropdownButtonStyle` | `borderless` `default` | | `headerActionsDisplayMode` | `buttons` `dropdown` | | `headerActionsPosition` | `start` `center` `end` | | `headerCenterElementsPosition` | `left` `center` `right` | | `paginatorStyle` | `default` `compact` `segmented` | | `paginatorVariant` | `full` `pager` `numbers` `simple` | | `paginatorAlign` | `start` `center` `end` | | `quickSearchCondition` | `%-%` `!%-%` `%-` `-%` `=` `!=` | | `quickSearchCaseMode`, `detailedSearchCaseMode` | `caseInsensitiveLatin` `caseSensitive` `uppercase` `lowercase` | | `columnSettingsMode` | `none` `localStorage` `dataSource` `host` | | `savedFiltersMode` | `localStorage` `dataSource` `host` | | `expandedDisplayMode` | `sidePanel` `inline` | | `inlineEditStartMode` | `action` `doubleClick` | | `orderDirection` | `asc` `desc` | --- ## 7. `dynamicTable`: editable repeating rows `dynamicTable` is the editable grid: users add and delete rows and type into cells. Its cells are declared in its own `columns` array, where **each entry is a full element definition**. ```json { "pages": [ { "name": "page1", "rows": [ { "columns": [{ "elementRef": "el1" }] }, { "columns": [{ "elementRef": "el2" }] } ] } ], "elements": { "page1": { "name": "page1", "label": "Invoice", "type": "page" }, "el1": { "name": "el1", "label": "Invoice lines", "type": "dynamicTable", "addRowButtonText": "Add line", "emptyMessage": "No lines yet", "maxRows": 50, "disallowAddRows": false, "disallowDeleteRows": false, "confirmRowDeletion": true, "confirmDeleteTitle": "Remove line", "confirmDeleteMessage": "Remove this invoice line?", "confirmDeleteConfirmLabel": "Remove", "confirmDeleteCancelLabel": "Keep", "hideRowIf": "{row.column5} == true", "columns": [ { "name": "column1", "label": "Product", "type": "select", "strictOptions": true, "options": [ { "value": "p1", "label": "Widget" }, { "value": "p2", "label": "Gadget" } ] }, { "name": "column2", "label": "Qty", "type": "number", "min": 1, "step": 1, "defaultValue": 1, "valueStorageType": "number" }, { "name": "column3", "label": "Unit price", "type": "number", "step": 0.01, "valueStorageType": "number", "visualFormatEnabled": true, "visualFormatMinFractionDigits": 2, "visualFormatMaxFractionDigits": 2 }, { "name": "column4", "label": "Line total", "type": "number", "readOnly": true, "valueStorageType": "number", "expression": "{row.column2} * {row.column3}", "logicExecutionMode": "onChange", "useTotals": true, "totalUseGrouping": true, "totalFractionDigits": 2, "totalLocale": "lt-LT" }, { "name": "column5", "label": "Cancelled", "type": "singleCheckbox", "checkboxLabel": "Cancelled", "defaultValue": false } ] }, "el2": { "name": "el2", "label": "Invoice total", "type": "number", "readOnly": true, "expression": "{el1.column4-total}", "logicExecutionMode": "onChange" } } } ``` The rules that make this work: - **Inside a row, other cells of the same row are `{row.columnX}`.** A bare `{column3}` reads a top-level form field with that name, not the cell. - `useTotals: true` publishes a footer sum. That sum is readable elsewhere as `{el1.column4-total}`: the table name, a dot, the column name, `-total`. `totalToData: true` publishes the sum without drawing the footer row. - `hideRowIf` is evaluated per row, also against `{row.*}`. - Column entries are element definitions, so any leaf element type and its own properties are valid there (`select` with `options`, `datepicker` with `format`, `text` with `maskType`, and so on). Value shape: an array of objects, one per row, keyed by column `name`. --- ## 8. `dynamicPanel`: repeating groups, with nesting `dynamicPanel` repeats a whole layout block. Its children live in the layout tree exactly like a `panel`'s, in `column.rows`. Each rendered entry gets its own copy. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "1000px", "widthUnit": "px" }, "pages": [ { "name": "page1", "rows": [ { "columns": [ { "elementRef": "el1", "rows": [ { "columns": [{ "elementRef": "el2" }, { "elementRef": "el3" }] }, { "columns": [{ "elementRef": "el4" }] }, { "columns": [{ "elementRef": "el5" }] } ] } ] }, { "columns": [{ "elementRef": "el6" }] }, { "columns": [{ "elementRef": "el7" }] } ] } ], "elements": { "page1": { "name": "page1", "label": "Projects", "type": "page" }, "el1": { "name": "el1", "label": "Projects", "type": "dynamicPanel", "addRowButtonText": "Add project", "removeRowButtonText": "Remove project", "emptyMessage": "No projects yet", "maxRows": 10, "confirmRowDeletion": true, "confirmDeleteTitle": "Remove project", "confirmDeleteMessage": "Remove this project and all of its tasks?", "panelPadding": "16px", "panelRadius": "8px", "panelBorderWidth": "1px", "contentGap": "12px", "resetChildrenOnHide": true }, "el2": { "name": "el2", "label": "Project name", "type": "text", "required": true }, "el3": { "name": "el3", "label": "Status", "type": "select", "strictOptions": true, "options": [ { "value": "open", "label": "Open" }, { "value": "done", "label": "Done" } ] }, "el4": { "name": "el4", "label": "Tasks", "type": "dynamicTable", "addRowButtonText": "Add task", "columns": [ { "name": "column1", "label": "Task", "type": "text" }, { "name": "column2", "label": "Hours", "type": "number", "step": 0.5, "valueStorageType": "number", "useTotals": true, "totalFractionDigits": 1 }, { "name": "column3", "label": "Rate", "type": "number", "step": 0.01, "valueStorageType": "number" }, { "name": "column4", "label": "Cost", "type": "number", "readOnly": true, "valueStorageType": "number", "expression": "{row.column2} * {row.column3}", "logicExecutionMode": "onChange", "useTotals": true, "totalFractionDigits": 2 } ] }, "el5": { "name": "el5", "label": "Project cost", "type": "number", "readOnly": true, "expression": "{panel.el4.column4-total}", "logicExecutionMode": "onChange" }, "el6": { "name": "el6", "label": "Total cost of all projects", "type": "number", "readOnly": true, "expression": "sumArray({el1}, el5)", "logicExecutionMode": "onChange" }, "el7": { "name": "el7", "label": "Open projects", "type": "number", "readOnly": true, "expression": "countInArray({el1}, el3 == \"open\")", "logicExecutionMode": "onChange" } }, "localization": { "defaultLanguage": "en", "languages": ["en"] } } ``` Context rules for nested structures, in one place: | Where you are | How you reach a value | | --- | --- | | Top level of the form | `{el1}` | | Inside a `dynamicTable` row | `{row.column2}` for a sibling cell | | Inside a `dynamicPanel` entry | `{panel.el3}` for a sibling field of the same entry | | A `dynamicTable` column total | `{el1.column4-total}` at top level | | A total inside a panel entry | `{panel.el4.column4-total}` | | A specific panel entry from outside | `{el1[0].el4.column4-total}` | | Across all entries of a panel | `sumArray({el1}, el5)`, `countInArray({el1}, el3 == "open")` | | A runtime variable | `{__variables.tenantId}` | | A table's own live state | `{__table.el2.selectedRows}`, `{__table.el2.totalRecords}` (see 6.5) | | Host-supplied external context | `{__external.userName}` | Inside collection functions the entry fields are written **bare and unquoted** (`el3 == "open"`), while anything outside the collection still uses braces (`role == {__variables.requiredRole}`). Prefer one aggregate over repeated accumulation. `setValue({var1}, sumArray({el1}, el5[].column3))` is safe on every recalculation; `sumValue` adds again on every run and belongs in an action, never in an `expression`. --- ## 9. Data sources and runtime variables ### 9.1 Every source type `dataSources[*]` is `{ name, title, type, params }`, where `params` differs per `type`. `type` is `rest`, `local`, `route` or `websocket`. ```json { "dataSources": [ { "name": "ds1", "title": "Customers (GET)", "type": "rest", "params": { "url": "/api/customers", "method": "GET" } }, { "name": "ds2", "title": "Save customer (POST)", "type": "rest", "params": { "url": "/api/customers/{id}", "method": "POST", "body": { "name": "{el1}", "segment": "{el2}", "tenantId": "{__variables.tenantId}" } } }, { "name": "ds3", "title": "Server-side table", "type": "rest", "params": { "url": "/api/customers/search", "method": "TABLE-POST" } }, { "name": "ds4", "title": "Static options", "type": "local", "params": { "localMode": "json", "dataJson": "[{\"id\":\"LT\",\"name\":\"Lithuania\"},{\"id\":\"LV\",\"name\":\"Latvia\"}]" } }, { "name": "ds5", "title": "Options already in the form data", "type": "local", "params": { "localMode": "dataPath", "dataPath": "el9.items" } }, { "name": "ds6", "title": "Resolved route data", "type": "route", "params": { "routeDataKey": "customer", "routeDataPath": "data" } }, { "name": "ds7", "title": "Live feed", "type": "websocket", "params": { "url": "wss://example.test/feed", "protocols": "", "message": "{\"subscribe\":\"orders\"}", "messagePath": "payload", "messageMode": "pushValuesInArray" } } ] } ``` REST URL and body templates use **single** braces: `{el1}`, `{row.id}`, `{__variables.tenantId}`. Method is one of `GET` `POST` `PUT` `PATCH` `DELETE`, plus the special `TABLE-POST` for lazy tables. ### 9.2 Element-level `dataSource` and cascading selects The element-level `dataSource` object is `IElementDataSource`: `name`, `useFor`, `optionValue`, `optionLabel`, `filterOptionsBy`, `params`, `refreshOnChange`, `refreshPaths`. Nothing else. ```json { "elements": { "el1": { "name": "el1", "label": "Country", "type": "select", "showSearch": true, "strictOptions": true, "dataSource": { "name": "ds1", "useFor": "option", "optionValue": "id", "optionLabel": "name" } }, "el2": { "name": "el2", "label": "City", "type": "select", "strictOptions": true, "disableIf": "isEmpty({el1})", "resetIf": "isEmpty({el1})", "logicExecutionMode": "onChange", "dataSource": { "name": "ds2", "useFor": "option", "optionValue": "id", "optionLabel": "name", "params": [{ "name": "country", "value": "{el1}" }], "refreshOnChange": true, "refreshPaths": ["el1"] } }, "el3": { "name": "el3", "label": "Warehouse", "type": "select", "strictOptions": true, "dataSource": { "name": "ds3", "useFor": "option", "optionValue": "id", "optionLabel": "title", "filterOptionsBy": "item.cityId == {el2} && item.active == true", "refreshPaths": ["el2"] } }, "el4": { "name": "el4", "label": "Loaded name", "type": "text", "expression": "{__variables.recordData}.name", "logicExecutionMode": "onChange" } }, "dataSources": [ { "name": "ds1", "title": "Countries", "type": "rest", "params": { "url": "/api/countries", "method": "GET" } }, { "name": "ds2", "title": "Cities", "type": "rest", "params": { "url": "/api/countries/{country}/cities", "method": "GET" } }, { "name": "ds3", "title": "Warehouses", "type": "rest", "params": { "url": "/api/warehouses", "method": "GET" } }, { "name": "ds4", "title": "Record", "type": "rest", "params": { "url": "/api/records/{__variables.recordId}", "method": "GET" } } ] } ``` - `useFor: "option"` feeds the option list; `useFor: "value"` feeds the element's value. - `params[*].value` resolves form fields, `{row.*}` and `{__variables.*}`; the resolved values also fill `{placeholder}` tokens in the source URL. - `filterOptionsBy` filters the fetched list client-side. The candidate option is `item.*`; form fields stay in braces. ### 9.3 Runtime variables ```json { "settings": { "language": "en", "variables": [ { "name": "recordId", "title": "Record id", "sourceType": "route", "source": "id" }, { "name": "tenantId", "title": "Tenant", "sourceType": "external", "source": "tenantId", "fallbackValue": "" }, { "name": "apiBase", "title": "API base", "sourceType": "constant", "constantValue": "/api/v2" }, { "name": "isEdit", "title": "Edit mode", "sourceType": "expression", "expression": "notEmpty({__variables.recordId})", "refreshPaths": [] }, { "name": "settingsData", "title": "Settings", "sourceType": "dataSource", "source": "ds1", "fallbackValue": {}, "includeInDataJson": false }, { "name": "draftName", "title": "Draft name", "sourceType": "manual", "targetPath": "el1", "includeInDataJson": true } ] } } ``` `sourceType` is exactly one of `route` `external` `constant` `expression` `dataSource` `manual`. Read a variable anywhere as `{__variables.name}`, never as `{name}`. A `dataSource` variable needs an explicit `source`. --- ## 10. Expression cookbook Every logic field is a **direct property of the element**. There is no `logic` wrapper. All of them take JEXL. ```json { "elements": { "el1": { "name": "el1", "label": "Price", "type": "number", "valueStorageType": "number" }, "el2": { "name": "el2", "label": "Quantity", "type": "number", "valueStorageType": "number" }, "el3": { "name": "el3", "label": "Total", "type": "number", "readOnly": true, "expression": "{el1} * {el2}", "logicExecutionMode": "onChange", "dependsOn": ["el1", "el2"] }, "el4": { "name": "el4", "label": "Customer type", "type": "radio", "showInline": true, "defaultValue": "person", "options": [ { "value": "person", "label": "Person" }, { "value": "company", "label": "Company" } ] }, "el5": { "name": "el5", "label": "Company code", "type": "text", "visibleIf": "{el4} == 'company'", "requireIf": "{el4} == 'company'", "resetIf": "{el4} != 'company'", "logicExecutionMode": "onChange" }, "el6": { "name": "el6", "label": "Discount %", "type": "number", "disableIf": "{el3} < 100", "readonlyIf": "{__variables.isEdit} != true", "logicExecutionMode": "onChange", "validators": [ { "type": "min", "value": 0, "message": "Cannot be negative" }, { "type": "max", "value": 50, "message": "50% is the maximum", "applyIf": "{el4} == 'person'" }, { "type": "custom", "condition": "{el6} > 0 && isEmpty({el7})", "message": "Give a reason for the discount" } ] }, "el7": { "name": "el7", "label": "Discount reason", "type": "textarea", "rows": 2 }, "el8": { "name": "el8", "label": "Starts", "type": "datepicker", "format": "YYYY-MM-DD" }, "el9": { "name": "el9", "label": "Ends", "type": "datepicker", "format": "YYYY-MM-DD", "minValue": "{el8}", "validators": [ { "type": "custom", "condition": "notEmpty({el8}) && notEmpty({el9}) && dateDiffDays({el8}, {el9}) < 1", "message": "End date must be after the start date" } ] }, "el10": { "name": "el10", "label": "Summary", "type": "text", "readOnly": true, "expression": "getLabel({el4}) + ': ' + {el3} + ' EUR'", "logicExecutionMode": "onChange" }, "el11": { "name": "el11", "label": "Tags", "type": "multiSelect", "options": [ { "value": "a", "label": "A" }, { "value": "b", "label": "B" } ] }, "el12": { "name": "el12", "label": "Has tag A", "type": "singleCheckbox", "checkboxLabel": "Tagged A", "readOnly": true, "expression": "contains({el11}, 'a')", "logicExecutionMode": "onChange" } } } ``` Operators JEXL supports: `&& || ! == != > >= < <= + - * / // % ^ in`, the ternary `a ? b : c`, and the Elvis `a ?: b`. It does **not** support `??`, `?.`, arrow functions or any other JavaScript-only syntax. Built-in functions: `isEmpty` `notEmpty` `toNumber` `inRange` `len` `contains` `containsAny` `containsAll` `inArray` `collectValuesFrom` `sumInArray` `avgInArray` `minInArray` `maxInArray` `countInArray` `firstInArray` `lastInArray` `joinInArray` `filterArray` `mapArray` `findInArray` `existsInArray` `sumArray` `avgArray` `getFirst` `getLast` `flattenArray` `today` `now` `date` `day` `month` `year` `weekDay` `weekDayIndex` `isWeekend` `addDays` `dateDiffDays` `startOfWeek` `endOfWeek` `getVal` `getValue` `getLabel` `getElementProperty` `getProp` `translate` `currentLanguage` `setValue` `sumValue` `pushValue` `setVar` `sumVar` `pushVar` `setElementProperty` `runDataSource` `dbg`. Validator objects accept exactly `type`, `value`, `message`, `condition`, `applyIf`. `condition` is the **failing** check: the error shows while it is `true`. --- ## 11. Actions and events `events` is an array of `IElementActionConfig`. Same shape for `button.events`, `table.rowActions`, `table.headerActions`, `table.selectionActions`, cell actions and dialog footer actions. ```json { "elements": { "el1": { "name": "el1", "label": "", "type": "button", "text": "Save", "variant": "solid", "tone": "primary", "icon": "save", "iconPosition": "left", "size": "normal", "fitContent": true, "events": [ { "trigger": "click", "type": "dataSource", "dataSourceName": "ds1", "validateForm": true, "confirmEnabled": true, "confirmTitle": "Save changes", "confirmMessage": "Write these changes to the server?", "confirmConfirmLabel": "Save", "confirmCancelLabel": "Cancel", "params": [ { "name": "id", "value": "{__variables.recordId}" }, { "name": "name", "value": "{el3}" } ], "responseMode": "setValue", "responseDataPath": "data.id", "responseTargetPath": "el4", "reloadOnReturnElementNames": ["el5"], "showToastAfter": true, "toastTitle": "Saved", "toastMessage": "The record was stored", "toastVariant": "success", "toastPosition": "bottom-right", "toastAutoHide": true, "toastAutoHideMs": 3000 } ], "menuActions": [ { "label": "Save and close", "type": "submit", "validateForm": true }, { "label": "Discard", "type": "navigate", "navigateTo": "/records" } ] }, "el2": { "name": "el2", "label": "", "type": "button", "text": "Download report", "variant": "outline", "tone": "neutral", "fitContent": true, "events": [ { "trigger": "click", "type": "dataSource", "dataSourceName": "ds2", "responseMode": "download", "responseFileName": "report.pdf" } ] }, "el3": { "name": "el3", "label": "Name", "type": "text" }, "el4": { "name": "el4", "label": "Saved id", "type": "text", "readOnly": true }, "el5": { "name": "el5", "label": "History", "type": "table", "columnsConfig": [] }, "el6": { "name": "el6", "label": "Segment", "type": "select", "options": [ { "value": "a", "label": "A" }, { "value": "b", "label": "B" } ], "events": [ { "trigger": "change", "type": "setOptions", "setOptionsTargetElement": "el7", "setOptionsMode": "contextPath", "setOptionsValue": "lookup.subSegments" }, { "trigger": "change", "type": "reloadElements", "reloadElementNames": ["el5"], "condition": "notEmpty({el6})" } ] }, "el7": { "name": "el7", "label": "Sub-segment", "type": "select", "options": [] } } } ``` Action fields by `type`: | `type` | Required / typical fields | | --- | --- | | `navigate` | `navigateTo`, `openInNewTab`, `beforeNavigateDataSourceName` | | `dataSource` | `dataSourceName`, `params`, `responseMode` (`none` `download` `setValue`), `responseDataPath`, `responseTargetElement`, `responseTargetPath`, `responseFileName`, `reloadCurrentElementAfterSuccess`, `reloadOnReturnElementNames`, `reloadDataSourceNames` | | `setValue` | `setValueTargetPath`, `setValueMode` (`contextPath` `template` `json` `expression`), `setValueValue` | | `setOptions` | `setOptionsTargetElement`, `setOptionsMode`, `setOptionsValue` | | `reloadElements` | `reloadElementNames` | | `toast` | `toastTitle`, `toastMessage`, `toastVariant`, `toastPosition`, `toastAutoHide`, `toastAutoHideMs`, `toastShowIcon`, `toastIcon` | | `dialog` | `dialogName`, `dialogOperation` (`open` `close` `toggle`) | | `sendMessage` | `dataSourceName` (a `websocket` source), `messagePayload` | | `validate` | none; validates the whole form | | `submit` | none; validates, then fires `onComplete` with `{ isValid, data, issues }` | Common to all: `trigger` (`click` `submit` `input` `change` `blur` `focus` `beforeLoad` `onLoad` `afterLoad` `always`), `label`, `icon`, `hideText`, `condition`, `validateForm`, `debounceMs`, `confirmEnabled`, `confirmOnDirty`, `confirmTitle`, `confirmMessage`, `confirmConfirmLabel`, `confirmCancelLabel`, `buttonVariant` (`filled` `outlined` `text`), `buttonTone` (`primary` `risk` `neutral`), `useCustomButtonStyle`, `showToastAfter` and the `toast*` fields. Note the two different button vocabularies: a `button` **element** uses `variant` (`solid` `outline` `text`) and `tone` (`primary` `neutral` `success` `info` `warning` `risk`); an **action** rendered as a button uses `buttonVariant` (`filled` `outlined` `text`) and `buttonTone` (`primary` `risk` `neutral`). --- ## 12. Multi-page stepper Several entries in `pages` become steps. Each still needs its twin in `elements`. ```json { "schemaVersion": 1, "settings": { "language": "en", "locale": "en-US", "width": "760px", "widthUnit": "px", "pageNavigationMode": "stepper", "stepperPosition": "top", "pageNavigationPosition": "bottom", "allowStepWithoutValidation": false, "actionButtonsPosition": "bottom", "showSubmitButton": true, "showValidateButton": false, "showValidationIssuesModal": true }, "pages": [ { "name": "page1", "rows": [{ "columns": [{ "elementRef": "el1" }, { "elementRef": "el2" }] }] }, { "name": "page2", "rows": [{ "columns": [{ "elementRef": "el3" }] }] }, { "name": "page3", "rows": [{ "columns": [{ "elementRef": "el4" }] }] } ], "elements": { "page1": { "name": "page1", "label": "Applicant", "type": "page" }, "page2": { "name": "page2", "label": "Address", "type": "page" }, "page3": { "name": "page3", "label": "Review", "type": "page", "visibleIf": "notEmpty({el1}) && notEmpty({el2})" }, "el1": { "name": "el1", "label": "First name", "type": "text", "required": true }, "el2": { "name": "el2", "label": "Last name", "type": "text", "required": true }, "el3": { "name": "el3", "label": "Street", "type": "text", "required": true }, "el4": { "name": "el4", "label": "", "type": "customHtml", "htmlTemplate": "

Submitting as {el1} {el2}, {el3}

" } }, "localization": { "defaultLanguage": "en", "languages": ["en", "de"], "texts": { "en": { "submitButtonText": "Submit" }, "de": { "submitButtonText": "Absenden" } } } } ``` --- ## 13. Pre-return checklist for any of these 1. Every `elementRef` resolves; every `elements` key equals its `name`. 2. Every `pages[*].name` has an `elements` twin with `type: "page"`. 3. No element definition holds children (`rows`, `columns`, `children`, `items`); the exception is `dynamicTable.columns` and `table.columnsConfig`, which are cell definitions, not layout. 4. No column object holds anything besides `elementRef`, `rows`, `tabRows`. 5. `table` columns use `key`. `dynamicTable` columns use `name`. 6. `statusRules[*].condition` uses `value`; `rowActions[*].condition` uses `row.*`. 7. Action and element-datasource `params` are `[{ "name", "value" }]`; table request params are `[{ "paramName", "paramValue" }]`. 8. Runtime variables are read as `{__variables.x}`. 9. Logic fields sit directly on the element; validators use `condition` / `applyIf`. 10. Every type string matches the canonical list exactly, casing included. --- ## AI: Legacy form migration Source: https://ngxviewbuilder.io/ai/legacy-form-migration # AI: Legacy form migration This page is intended for the AI agent when a user asks to convert a form defined in a legacy form-builder JSON format to NGX View Builder JSON. ## Core rule - Do not copy the source JSON 1:1. - First recognize the semantics of the source form. - Then map them to the closest supported NGX View Builder structure. ## Element map | Source JSON | NGX View Builder | | --- | --- | | `text` | `text` or `number` / `phoneInput` depending on `inputType` | | `comment` | `textarea` | | `radiogroup` | `radio` | | `dropdown` | `select` | | `dropdown_list` | `autocomplete` | | `tagbox` | `multiSelect` | | `checkbox` | `checkbox` or `singleCheckbox` | | `boolean` | `singleCheckbox` | | `date` / `date_picker` | `datepicker` | | `file_uploader` / `file_list` | `fileUpload` | | `html` | `customHtml` | | `message_box` | `messageCard` | | `panel` | `panel` | | `paneldynamic` | `dynamicPanel` | | `matrixdynamic` / `matrixdropdown` | `dynamicTable` | | `dialog` | `dialog` | ## Property map - `visibleIf` maps directly to `visibleIf`: same field name, same polarity, no inversion needed. - `enableIf` is typically mapped to `disableIf`, but the logic must be inverted. - `requiredIf` maps to `requireIf`. - `resetValueIf` maps to `resetIf`. - `setValueExpression` typically maps to `expression`. - `defaultValue` stays as `defaultValue` if it is a static value. - `defaultValueExpression` can become `defaultValue` if it is a literal, or `expression` if it is a computed value. - `choices` maps to `options`. - `validators` maps to `validators`. ## Expression translation rules In the final NGX View Builder JSON, use NGX View Builder / `JEXL`-style syntax. ### Operators - `and` -> `&&` - `or` -> `||` - `<>` -> `!=` - `=` in comparisons -> `==` - `iif(cond, a, b)` -> `cond ? a : b` ### Empty values - `x notempty` -> `notEmpty(x)` - `x empty` -> `isEmpty(x)` ### Collections - `anyof` -> `containsAny(left, values)` - `allof` -> `containsAll(left, values)` - `contains` -> `contains(left, value)` - `{field.length}` -> `len({field})` ### Dynamic contexts - `{panelIndex}` -> `panel.index` - `{rowIndex}` -> `row.index` - `{parentIndex}` -> `parentIndex` - `{panel.field}` -> `panel.field` - `{row.field}` -> `row.field` ## Important safeguards - Do not use source-library property names in the final NGX View Builder JSON if NGX View Builder does not have that property. - Do not use `and`, `or`, `notempty`, `empty`, `<>`, or `iif(...)` in final NGX View Builder expression strings. - Do not use self-reference expressions. A field must not reference itself in a fallback branch. - If `expression`, `visibleIf`, `disableIf`, `requireIf`, `readonlyIf`, or `resetIf` is added, `logicExecutionMode: "onChange"` is typically required. ## When a 1:1 mapping is not possible - If a source-library feature has no direct NGX View Builder equivalent, produce the closest safe result. - In that case, briefly explain in a `warnings` section what was simplified or lost. ---