Skip to content

Latest commit

 

History

History
1692 lines (1539 loc) · 65.6 KB

File metadata and controls

1692 lines (1539 loc) · 65.6 KB

Dynamic and Vue Controllers

FwDynamicController allows building CRUD controllers driven entirely by a JSON configuration file. The configuration lives in /template/CONTROLLER/config.json and defines which model is used, what fields are visible and how list and form screens behave. FwVueController extends this approach and renders list and form pages using Vue.js so that items can be edited inline.

Below is a description of the configuration format that controls behaviour of both controllers.

Start Here

  • Use this doc when a controller inherits FwDynamicController or FwVueController, or when a task changes config.json.
  • Read the top sections first for shared keys and common patterns.
  • Use the type reference later as a lookup table when you need the exact options for a single field type.

config.json for Dynamic/Vue controllers

In FwDynamicController, controller behaviour is defined by /template/CONTROLLER/config.json. A sample file can be found at /template/admin/demosdynamic/config.json.

Common config keys include:

  • model – main model name
  • required_fields – space separated required fields
  • save_fields and save_fields_checkboxes – fields saved from ShowForm
  • form_new_defaults – defaults for new item
  • search_fields – fields used for searching and for the default list search placeholder
  • list_sortdef and list_sortmap – default sorting
  • related_field_name – related id field
  • list_view – table or SQL used for listing
  • is_dynamic_index – enable dynamic list with view_list_defaults and view_list_map
  • is_dynamic_index_edit – allow inline editing (list_edit, edit_list_defaults)
  • view_list_custom – fields visible by default
  • list_calculated_fields – virtual list fields and the source columns needed to calculate them
  • list_column_filters – optional typed per-column filters for dynamic/Vue list tables
  • view_list_custom_trusted - subset of view_list_custom fields allowed to render cellFormatter HTML in Vue lists; other custom cells are escaped text by default
  • is_dynamic_show and is_dynamic_showform – enable dynamic screens
  • form_tabs – optional tab definitions
  • route_return – action to redirect after save
  • is_userlists – enable UserLists support
  • is_readonly – when true, render the controller read-only and block standard mutating actions

"show_fields" and "showform_fields"

Each entry in show_fields or showform_fields configures one block on the Show or ShowForm screen. Use the reference below to pick a type, set the required keys, and copy an example you can paste into config.json.

Common field keys

  • type: required type identifier (see the type reference below).
  • field: database column or virtual name; some non-data blocks do not need it.
  • label: text for the visible label.
  • lookup_model / lookup_tpl / lookup_table / lookup_field / lookup_key / lookup_params: sources for lookup values (model-based, template-based or direct table lookup).
  • class / attrs / class_label / class_contents: wrapper and label sizing/styling.
  • class_control / attrs_control: styling and behaviour on the control itself. On classic form posts, use on-refresh to resubmit the form without saving on change, or on-refresh-save to validate/save first and reload ShowForm.
  • help_text: muted helper text under the control.

Input-only helpers

  • required, maxlength, max, min, step, placeholder, rows: standard HTML validation/appearance options.
  • validate: simple validation codes (exists, isemail, isphone, isdate, isfloat).
  • is_inline: render radio/yesno choices inline.
  • autocomplete_url: data source for autocomplete fields (called as ?q=...).
  • conv: converter for display/save (for example time_from_seconds).
  • default_time: default time for datetime_popup.
  • is_custom: mark placeholder for manual processing.

Lookup and filtering helpers

  • is_option0 / is_option_empty / option0_title: include a blank option in select fields.
  • filter_for / filter_by / filter_field: wire dependent selects (example provided in the select type section).
  • lookup_by_value: for autocomplete fields, store the typed value instead of id.
  • lookup_id / admin_url: build links for plaintext_link.

Calculated list columns

Use list_calculated_fields when a name in view_list_map is populated by controller or model code after the list query instead of coming from list_view. Map each calculated field to the simple source column names needed to calculate it:

"view_list_defaults": "title full_name",
"view_list_map": {
  "title": "Title",
  "full_name": "Full name"
},
"list_calculated_fields": {
  "full_name": ["first_name", "last_name"]
}

Dependency values may be a space-separated string or an array. Use an empty array for a calculated field that needs no source columns. Calculated names must exist in view_list_map; names and dependencies must be simple identifiers. Unknown or unsafe metadata is ignored rather than added to SQL.

For existing Vue configurations, when the top-level key is absent, store.list_calculated_fields dictionaries retain their previous source-column-to-calculated-name meaning and are adapted on the server. New top-level dictionaries always mean calculated-name-to-dependencies. When neither configuration key exists, init does not overwrite a client-only fwStoreState calculated-field setting; move that setting into controller metadata to gain server projection and query protection.

For both Dynamic and Vue lists, populate the calculated value in a controller getListRows() override after calling base.getListRows(). Vue lists can also use the model's filterForJson() implementation; classic Dynamic lists do not call that hook. The framework excludes calculated names from SELECT clauses, keyword search, per-column filters, search hints, and the automatically generated sort map. An explicit list_sortmap entry may map a calculated UI name to a real database sort field when the application can support that ordering safely.

Only dependencies for calculated fields in the active user view are selected. The record id is still selected for list actions. Dependencies added only for calculation are removed from Vue JSON after controller and model row shaping; a dependency that is itself a visible list field remains in the response. CSV/XLS exports use the same calculation and pruning path and export the selected calculated values.

List column filters

list_column_filters.enabled opts a Dynamic or Vue list into typed per-column filters. The default is disabled, and plain FwController screens keep the legacy text-only search[field] behavior.

"list_column_filters": {
  "enabled": true,
  "fields": {
    "iname": { "type": "text" },
    "fdate_pop": { "type": "date_range" },
    "demo_dicts_id": {
      "type": "multi_select",
      "lookup_model": "DemoDicts"
    },
    "status": {
      "type": "multi_select",
      "lookup_tpl": "/common/sel/status.sel"
    },
    "ffloat": { "type": "number_conditions" },
    "is_active": { "type": "boolean" },
    "large_lookup_id": {
      "type": "autocomplete",
      "autocomplete_url": "/Admin/LargeLookups/(Autocomplete)?q="
    },
    "unsafe_calc": { "type": "none" }
  }
}

Supported types are text, date_range, multi_select, autocomplete, number_conditions, boolean, and none. Every type supports blank/not blank. Text filters render inline as a compact operator/input group with operator labels ~, =, !=, !~, ^, !^, $, !$, B, and NB; the empty operator is the default and behaves like contains. Structured filters render as one-line dropdown cells; changes inside the dropdown are draft-only until the user clicks Apply, while Clear removes that column's search[field] value.

Text filters preserve the legacy search[field] syntax (abc, =abc, !=abc, !abc, ^abc for starts-with, $abc for ends-with, !^abc for does-not-start-with, and !$abc for does-not-end-with) and also accept JSON such as {"type":"text","op":"starts_with","value":"abc"}. Number conditions support equal, not_equal, gte, lte, inclusive from/to, and strict not_between_from/not_between_to. Typed filters submit JSON through the same search[field] key and are converted to parameterized SQL server-side.

fields is optional. When it is omitted, Dynamic infers typed filters for visible simple fields from form definitions and table schema. Fields declared in list_calculated_fields are always non-filterable. Other aliases, dotted fields, and expressions need an explicit entry. filter_field can point a visible list alias to the real simple list column used in SQL predicates. lookup_model, lookup_tpl, and inline options reuse the same option conventions as form fields; lookup models load active rows by default, so use explicit type: "autocomplete" for large lookup tables. Date range filters use user-local date input and apply the framework timezone rules for real datetime columns; set is_date_only: true when a datetime-backed field is semantically a date-only UI value.

To customize an inferred filter, add the field under list_column_filters.fields and override only the parts that differ:

"list_column_filters": {
  "enabled": true,
  "fields": {
    "customer_iname": {
      "type": "autocomplete",
      "filter_field": "customers_id",
      "autocomplete_url": "/Admin/Customers/(Autocomplete)?q="
    },
    "status": {
      "type": "multi_select",
      "options": {
        "0": "Active",
        "10": "Pending"
      }
    }
  }
}

For custom server behavior in a Dynamic controller, override applyListColumnFilter(FwDict def, FwDict rawValue) and return true after appending a safe predicate. Read only your whitelisted field, and put user values into list_where_params. The normalized def includes field, field_name, type, and filter_field:

protected override bool applyListColumnFilter(FwDict def, FwDict rawValue)
{
    if (def["field_name"].toStr() != "score_band")
        return false;

    var band = rawValue["value"].toStr();
    if (band == "high")
    {
        list_where += " AND [score] >= @score_band";
        list_where_params["score_band"] = 80;
        return true;
    }

    return false;
}

For custom UI on server-rendered Dynamic screens, set template to "custom" and add index/list_filter_custom.html under the controller template directory. That controller-local partial is included from the common filter cell and can branch on filter[field]; it should render a compact one-line trigger or input and write committed JSON to search[field].

"score_band": {
  "type": "text",
  "template": "custom"
}

For Vue screens, set component to a registered Vue component name. The component receives header, owns its compact/dropdown UI, writes header.search_value, and calls fwStore.setFilters({}) to reload:

"score_band": {
  "type": "text",
  "component": "orders-score-band-filter"
}

Vue header metadata is nested under header.filter; use header.filter.type, header.filter.options, header.filter.autocomplete_url, and header.filter.component rather than flattened header keys.

Generated layout heuristics

Developer-generated and virtual controllers build a two-column show_fields / showform_fields layout from schema metadata when a controller-specific config does not define those arrays.

  • Schema-detected computed/generated columns stay visible in lists and forms, but their generated edit definition is read-only plaintext and they are omitted from generated save_fields. Explicit stored or file-based controller config remains authoritative.
  • The bundled Demos kitchen-sink example mirrors this contract across its classic, Dynamic, and Vue controllers: icode and iname are editable, while database-computed display_name (CODE — Title) is visible, searchable/sortable, and read-only.
  • Wide content fields such as markdown, textareas, raw HTML, and subtables are kept in the primary content column.
  • Major lookup identity fields such as iname and icode stay in the primary content column.
  • Compact non-system fields on larger generated forms are balanced into the right-side column only when the right column stays visually lighter after the field is added.
  • Framework and lifecycle metadata such as id, status, add_time, upd_time, and applied_time stays in the right-side metadata area.
  • Minor ordering fields such as prio stay in the right-side metadata area.
  • Right-side support fields render after id and before bottom metadata such as prio, status, add_time, and upd_time.
  • Attachment-heavy controls stay in the right-side support area by default.
  • Explicit UI: formcol=left|mid|right overrides the generated placement; mid and right both target the right-side column. class and class_contents tune wrapper/control sizing after placement.

Attachments and subtables

  • att_category / att_post_prefix: configure upload bucket and input prefix for att* fields.
  • model / save_fields / save_fields_checkboxes / required_fields / is_by_linked: configure multi* and subtable* blocks.
  • Dynamic SaveAttFiles and existing-row subtable_edit saves authorize against the current parent row before object-link side effects run; override the controller's row-loading scope for app-specific ownership rules.
  • Dynamic att_links_edit saves call Att.checkAccess(att_id, "link", fwentities_id, item_id) before changing att_links; the default link policy allows active unbound library attachments and active attachments already bound to the same target, and rejects attachments bound to another entity/item.

Button addons (Dynamic and Vue)

  • prepend / append: arrays of buttons rendered before/after controls.
    [
      {
        "event": "add", // only used in FwVueController
        "class": "",
        "icon": "bi bi-plus",
        "label": "",
        "hint": "Add New"
      }
    ]

Dynamic and Vue button configs still use icon CSS class strings such as "bi bi-plus". Static ParsePage templates should use common/icons/* partials, but generated/config-driven buttons are not fully icon-library-agnostic until their icon contract is migrated in a separate pass.

Type reference (TOC)

Layout helpers: row · col · col_end · row_end · header

Show & ShowForm display: plaintext · plaintext_json · plaintext_link · plaintext_autocomplete · plaintext_yesno · plaintext_currency · markdown · noescape · float · range · checkbox · switch · date · date_long · multi · multi_prio · att · att_links · att_files · subtable · added · updated

ShowForm inputs only: group_id · group_id_addnew · select · input · textarea · email · number · range · password · currency · autocomplete · multicb · multicb_prio · radio · yesno · cb · switch · date_popup · date_combo · datetime_popup · datetime_local · time · att_edit · att_links_edit · att_files_edit · subtable_edit

Type details

type: row

  • Template: /common/form/showform/row.html (also used on Show).
  • Options: class (extra classes on .row), attrs (custom attributes).
  • Common sample:
{
  "type": "row"
}
  • Full sample with custom gutter and data attribute:
{
  "type": "row",
  "class": "g-3 align-items-center",
  "attrs": "data-block=\"main\""
}

type: col

  • Template: /common/form/showform/col.html (also used on Show).
  • Options: class (size overrides such as col-md-6), attrs.
  • Common sample:
{
  "type": "col",
  "class": "col-md-6"
}
  • Full sample that nests additional attributes:
{
  "type": "col",
  "class": "col-lg-4 mb-3",
  "attrs": "data-role=\"meta\""
}

type: col_end

  • Template: /common/form/showform/col_end.html (also used on Show).
  • Options: none (use to close the previous col).
  • Common sample:
{
  "type": "col_end"
}
  • Full sample in a two-column layout:
[
  { "type": "col", "class": "col-md-6" },
  { "type": "header", "label": "Left" },
  { "type": "col_end" },
  { "type": "col", "class": "col-md-6" },
  { "type": "header", "label": "Right" },
  { "type": "col_end" }
]

type: row_end

  • Template: /common/form/showform/row_end.html (also used on Show).
  • Options: none (use to close the previous row).
  • Common sample:
{
  "type": "row_end"
}
  • Full sample wrapping two columns:
[
  { "type": "row" },
  { "type": "col", "class": "col-md-6" },
  { "type": "col_end" },
  { "type": "col", "class": "col-md-6" },
  { "type": "col_end" },
  { "type": "row_end" }
]

type: header

  • Template: /common/form/showform/header.html.
  • Options: label (required), plus wrapper class/attrs if using surrounding row/col.
  • Common sample:
{
  "type": "header",
  "label": "General"
}
  • Full sample with extra spacing:
{
  "type": "header",
  "label": "Metadata",
  "class": "mt-4",
  "attrs": "data-section=\"meta\""
}

type: plaintext

  • Template: /common/form/show/plaintext.html.
  • Options: inherits common layout keys plus single-value lookups (lookup_model/lookup_field, lookup_table/lookup_key, lookup_tpl, or inline options). conv: "time_from_seconds" converts stored seconds to HH:mm:ss.
  • Common sample:
{
  "type": "plaintext",
  "field": "iname",
  "label": "Title"
}
  • Full sample with lookup and custom layout:
{
  "type": "plaintext",
  "field": "category_id",
  "label": "Category",
  "lookup_model": "DemoDicts",
  "lookup_field": "iname",
  "class_label": "col-md-2",
  "class_contents": "col-md-10",
  "help_text": "Resolved via DemoDicts lookup"
}

type: plaintext_json

  • Template: /common/form/show/plaintext_json.html.
  • Options: inherits common layout keys. Valid JSON is pretty-printed for Dynamic and Vue Show/ShowForm display; invalid or blank text is shown unchanged and escaped as text.
  • Common sample:
{
  "type": "plaintext_json",
  "field": "metadata_json",
  "label": "Metadata"
}
  • Full sample with custom layout:
{
  "type": "plaintext_json",
  "field": "payload_json",
  "label": "Payload",
  "class_label": "col-md-2",
  "class_contents": "col-md-10",
  "help_text": "Stored JSON rendered as read-only formatted text"
}

type: plaintext_link

  • Template: /common/form/show/plaintext_link.html.
  • Options: single-value lookups plus admin_url (destination path for the link) and optional lookup_id override.
  • Common sample:
{
  "type": "plaintext_link",
  "field": "user_id",
  "label": "Owner",
  "lookup_model": "Users"
}
  • Full sample with explicit lookup table and admin URL:
{
  "type": "plaintext_link",
  "field": "manager_id",
  "label": "Manager",
  "lookup_table": "users",
  "lookup_key": "id",
  "lookup_field": "email",
  "admin_url": "/Admin/Users",
  "help_text": "Links to the user record"
}

type: plaintext_autocomplete

  • Template: /common/form/show/plaintext_autocomplete.html.
  • Options: same lookup keys as plaintext; intended for ids saved by an autocomplete control.
  • Common sample:
{
  "type": "plaintext_autocomplete",
  "field": "demo_dicts_id",
  "label": "Category",
  "lookup_model": "DemoDicts"
}
  • Full sample resolving from a lookup table with custom label sizing:
{
  "type": "plaintext_autocomplete",
  "field": "parent_id",
  "label": "Parent Demo",
  "lookup_table": "demos",
  "lookup_field": "iname",
  "class_label": "col-sm-2",
  "class_contents": "col-sm-10"
}

type: plaintext_yesno

  • Template: /common/form/show/plaintext_yesno.html.
  • Options: uses the truthiness of value; combine with conv: "time_from_seconds" only for time fields.
  • Common sample:
{
  "type": "plaintext_yesno",
  "field": "is_active",
  "label": "Active"
}
  • Full sample with helper text:
{
  "type": "plaintext_yesno",
  "field": "is_verified",
  "label": "Email Verified",
  "help_text": "Derived from verification timestamp"
}

type: plaintext_currency

  • Template: /common/form/show/plaintext_currency.html.
  • Options: currency_symbol (defaults to $) plus common layout keys.
  • Common sample:
{
  "type": "plaintext_currency",
  "field": "price",
  "label": "Price"
}
  • Full sample with alternate currency symbol and help text:
{
  "type": "plaintext_currency",
  "field": "budget",
  "label": "Budget",
  "currency_symbol": "€",
  "class_label": "col-md-2",
  "class_contents": "col-md-4",
  "help_text": "Formatted with two decimals"
}

type: markdown

  • Template: /common/form/show/markdown.html.
  • Options: markdown comes from value; respect common layout keys. Raw HTML is disabled by default. For classic, Vue, or extracted read-only markdown from server-controlled or already-sanitized content, set "trusted": true to allow raw HTML.
  • Common sample:
{
  "type": "markdown",
  "field": "idesc",
  "label": "Description"
}
  • Full sample highlighting read-only rendering:
{
  "type": "markdown",
  "field": "notes_md",
  "label": "Notes (Markdown)",
  "class_contents": "col-12",
  "help_text": "Rendered server-side with links and formatting"
}
  • Full sample for trusted snippets:
{
  "type": "markdown",
  "field": "trusted_html_md",
  "label": "Trusted Markdown",
  "class_contents": "col-12",
  "trusted": true,
  "help_text": "Content is not sanitized here; ensure it is trusted upstream"
}

type: noescape

  • Template: /common/form/show/noescape.html.
  • Options: classic templates and Vue views render raw HTML.
  • Use only for server-controlled content or content already sanitized by a trusted upstream policy.
  • Common sample:
{
  "type": "noescape",
  "field": "html_block",
  "label": "Raw HTML"
}
  • Full sample for trusted snippets:
{
  "type": "noescape",
  "field": "widget_embed",
  "label": "Embed",
  "class_contents": "col-12",
  "help_text": "Content is not escaped; ensure it is sanitized upstream"
}

type: float

  • Template: /common/form/show/float.html.
  • Options: displays value with two decimals; common layout keys apply.
  • Common sample:
{
  "type": "float",
  "field": "amount",
  "label": "Amount"
}
  • Full sample with helper text:
{
  "type": "float",
  "field": "tax_rate",
  "label": "Tax Rate",
  "help_text": "Stored as decimal, rendered with 2 digits"
}

type: checkbox

  • Template: /common/form/show/checkbox.html.
  • Options: uses truthy value; combine with layout keys.
  • Common sample:
{
  "type": "checkbox",
  "field": "is_done",
  "label": "Completed"
}
  • Full sample with muted hint:
{
  "type": "checkbox",
  "field": "is_featured",
  "label": "Featured?",
  "help_text": "Checked when the stored value is truthy"
}

type: date

  • Template: /common/form/show/date.html.
  • Options: displays value using the current user's date format; use conv: "time_from_seconds" only when the field stores seconds.
  • Rendering rule: intended for calendar dates. Date-only values are not shifted by fw.userTimezone.
  • Common sample:
{
  "type": "date",
  "field": "due_date",
  "label": "Due"
}
  • Full sample with label sizing:
{
  "type": "date",
  "field": "ship_on",
  "label": "Ship On",
  "class_label": "col-md-2",
  "class_contents": "col-md-4",
  "help_text": "Uses user date formatting"
}

type: date_long

  • Template: /common/form/show/date_long.html.
  • Options: renders value in the current user's date/time format with seconds; combine with common layout keys.
  • Rendering rule: intended for true datetimes, so output is timezone-adjusted for the current user.
  • Common sample:
{
  "type": "date_long",
  "field": "updated_time",
  "label": "Updated"
}
  • Full sample with helper text:
{
  "type": "date_long",
  "field": "processed_at",
  "label": "Processed",
  "help_text": "Includes time in the user timezone"
}

type: multi

  • Template: /common/form/show/multi.html.
  • Options: lookup_model (+ lookup_field, lookup_params) to render values without a junction model; model to use a junction table; is_by_linked to switch between updateJunctionByMainId and updateJunctionByLinkedId; lookup_checked_only to show only checked rows.
  • Common sample (comma-separated ids in the same table field):
{
  "type": "multi",
  "field": "tag_ids",
  "label": "Tags",
  "lookup_model": "DemoDicts",
  "lookup_field": "iname"
}
  • Full sample using a junction model and showing only checked records:
{
  "type": "multi",
  "field": "demo_dicts_link",
  "label": "DemoDicts via Junction",
  "model": "DemosDemoDicts",
  "is_by_linked": false,
  "lookup_checked_only": true,
  "help_text": "Rendered from the junction table for this record"
}

type: multi_prio

  • Template: no dedicated Show partial is registered; add a custom template or reuse the multi template to display multi_datarow (which includes _link[prio]).
  • Options: model (required, junction model providing _link[prio]), is_by_linked to flip main/linked behaviour, plus layout keys.
  • Common sample:
{
  "type": "multi_prio",
  "field": "roles",
  "label": "Roles",
  "model": "UsersRoles"
}
  • Full sample with custom ordering note:
{
  "type": "multi_prio",
  "field": "permissions",
  "label": "Permissions (prio)",
  "model": "RolesPermissions",
  "is_by_linked": true,
  "help_text": "Template shows checkboxes; extend the template to display priorities from _link[prio]"
}

type: att

  • Template: /common/form/show/att.html.
  • Options: relies on field holding an attachment id; layout keys apply (category is determined by the stored attachment).
  • Common sample:
{
  "type": "att",
  "field": "photo",
  "label": "Photo"
}
  • Full sample with custom column width:
{
  "type": "att",
  "field": "avatar_id",
  "label": "Avatar",
  "class_contents": "col-md-4",
  "help_text": "Shows the linked attachment preview and download"
}

type: att_links

  • Template: /common/form/show/att_links.html.
  • Options: lists attachments linked to the current entity; field may be empty because lookup uses the entity table/id.
  • Common sample:
{
  "type": "att_links",
  "field": "_att_links",
  "label": "Documents"
}
  • Full sample scoped by label styling:
{
  "type": "att_links",
  "field": "_att_links",
  "label": "Supporting Files",
  "class_label": "col-12",
  "help_text": "Displays all attachments linked to this record"
}

type: att_files

  • Template: /common/form/show/att_files.html.
  • Options: att_category (filter by category), att_post_prefix (reserved for parity with edit configuration), plus layout keys.
  • Common sample:
{
  "type": "att_files",
  "field": "_att_files",
  "label": "Files",
  "att_category": "general"
}
  • Full sample filtered to a category with helper text:
{
  "type": "att_files",
  "field": "_att_files_docs",
  "label": "Documents",
  "att_category": "docs",
  "class_contents": "col-12",
  "help_text": "Lists files in the docs attachment category"
}

type: subtable

  • Template: /common/form/show/subtable.html.
  • Options: model (required), plus any model-specific settings such as related_field_name, lookup_params, or a custom subtable template used by the model’s prepareSubtable.
  • Common sample:
{
  "type": "subtable",
  "field": "items",
  "label": "Items",
  "model": "DemosItems"
}
  • Full sample with custom label styling and params passed to the model:
{
  "type": "subtable",
  "field": "line_items",
  "label": "Line Items",
  "model": "OrdersItems",
  "class_label": "col-12 hr-header fs-5",
  "lookup_params": "with_products",
  "help_text": "Rendered via OrdersItems.prepareSubtable"
}

type: added

  • Template: /common/form/show/added.html.
  • Options: layout keys only (field value is taken from the model metadata).
  • Common sample:
{
  "type": "added",
  "label": "Added"
}
  • Full sample with explicit field name:
{
  "type": "added",
  "field": "add_time",
  "label": "Created",
  "class_contents": "col-md-4"
}

type: updated

  • Template: /common/form/show/updated.html.
  • Options: layout keys only (field value is taken from the model metadata).
  • Common sample:
{
  "type": "updated",
  "label": "Updated"
}
  • Full sample targeting a specific field:
{
  "type": "updated",
  "field": "upd_time",
  "label": "Last Updated",
  "class_contents": "col-md-4"
}

type: group_id

  • Template: /common/form/showform/group_id.html.
  • Options: layout keys only; renders the item id with Save/Cancel buttons.
  • Common sample:
{
  "type": "group_id"
}
  • Full sample with custom wrapper class:
{
  "type": "group_id",
  "class": "mt-2"
}

type: group_id_addnew

  • Template: /common/form/showform/group_id_addnew.html.
  • Options: layout keys only; adds “Save and Add New”.
  • Common sample:
{
  "type": "group_id_addnew"
}
  • Full sample with helper text:
{
  "type": "group_id_addnew",
  "help_text": "Adds Save and Save & Add New buttons"
}

type: select

  • Template: /common/form/showform/select.html.
  • Options:
    • Data sources: lookup_model (+ lookup_params), lookup_tpl, or inline options dictionary.
    • Blank options: is_option0 (value 0), is_option_empty (empty value), option0_title.
    • Filtering chains: filter_for/filter_field on the parent select; filter_by/filter_field on the child select.
    • Status handling: model lookups return active rows by default. On edit forms, the currently saved inactive lookup row is included only for that same field and labeled (inactive). Override the lookup model method when a project needs broader status coverage.
    • Behaviour: multiple, class_control, attrs_control, err_exists_msg, prepend/append input-group buttons.
    • Dropdown buttons: prepend/append can include dropdown definitions with:
      • is_dropdown (true to render Bootstrap 5 dropdown)
      • is_split (optional, default false)
      • class, icon, label, hint, url, attrs
      • items array with is_divider, class, icon, label, url, attrs
      • url supports {id} placeholder for edit actions that need current select value.
  • Common sample:
{
  "type": "select",
  "field": "demo_dicts_id",
  "label": "DemoDicts",
  "lookup_model": "DemoDicts",
  "is_option0": true,
  "class_control": "on-refresh"
}
  • On classic form posts, use "class_control": "on-refresh-save" instead when the changed value and the rest of the form must be saved before dependent fields are refreshed.
  • Full sample with filtering and live search:
{
  "type": "select",
  "field": "parent_id",
  "label": "Parent",
  "lookup_model": "Demos",
  "lookup_params": "parent",
  "filter_by": "parent_demo_dicts_id",
  "filter_field": "demo_dicts_id",
  "is_option_empty": true,
  "option0_title": "- none -",
  "multiple": false,
  "class_contents": "col-md-3",
  "class_control": "select2 on-refresh",
  "attrs_control": "data-live-search=\"true\" data-noautosave=\"true\"",
  "prepend": [
    {
      "label": "Add",
      "class": "btn-secondary",
      "icon": "bi bi-plus",
      "url": "/Admin/Demos/new"
    }
  ]
}
  • Dropdown example with lookup add/edit modal:
{
  "type": "select",
  "field": "demo_dicts_id",
  "label": "DemoDicts",
  "lookup_model": "DemoDicts",
  "is_option_empty": true,
  "append": [
    {
      "is_dropdown": true,
      "class": "btn-secondary",
      "icon": "bi bi-three-dots-vertical",
      "label": "Actions",
      "items": [
        {
          "class": "on-fw-modal",
          "icon": "bi bi-plus",
          "label": "Add",
          "url": "/Admin/DemoDicts/new?",
          "attrs": "data-fw-lookup=\"add\" data-modal-focus=\"#iname\""
        },
        {
          "class": "on-fw-modal",
          "icon": "bi bi-pencil",
          "label": "Edit",
          "url": "/Admin/DemoDicts/{id}/edit?",
          "attrs": "data-fw-lookup=\"edit\" data-modal-focus=\"#iname\""
        },
        {
          "is_divider": true
        },
        {
          "label": "Manage",
          "url": "/Admin/DemoDicts?"
        }
      ]
    }
  ]
}

Lookup modal saves update the source control, select the saved value, emit a bubbling fw-lookup-saved event from the target control, and then emit the normal change event for compatibility. Use the custom event when screen-specific JavaScript needs the saved lookup details:

document.addEventListener('fw-lookup-saved', function (e) {
  if (e.target.name !== 'item[demo_dicts_id]') return;
  console.log(e.detail.id, e.detail.label, e.detail.mode);
});

Lookup add/edit modals namespace loaded content IDs by default to avoid duplicate DOM IDs when a modal form has fields with the same IDs as the parent page. Set data-fw-modal-namespace-ids="0" on the modal trigger only for legacy modal content that still depends on global #id selectors. New modal scripts should use scoped selectors such as $(fw.scopeFromScript()).find('[name="item[iname]"]').

Remote modals focus the first visible form control after content loads. Use data-modal-focus="#iname" in a button's attrs to focus a specific modal field; simple #id selectors still work in namespaced lookup modals because the modal also matches the original data-fw-original-id. Use data-modal-focus="none" to keep Bootstrap's default focus behavior.

type: input

  • Template: /common/form/showform/input.html.
  • Options: maxlength, placeholder, required, validate (exists, isemail, isphone, isdate, isfloat), class_control, attrs_control, prepend/append input-group buttons.
  • Common sample:
{
  "type": "input",
  "field": "iname",
  "label": "Title",
  "maxlength": 255
}
  • Full sample with validation and addon button:
{
  "type": "input",
  "field": "slug",
  "label": "Slug",
  "required": true,
  "maxlength": 128,
  "validate": "exists",
  "placeholder": "auto-generated or custom",
  "class_control": "text-lowercase",
  "append": [
    {
      "label": "Generate",
      "class": "btn-outline-secondary",
      "icon": "bi bi-magic",
      "url": "/Admin/DemosDynamic/(GenerateSlug)"
    }
  ]
}

type: textarea

  • Template: /common/form/showform/textarea.html.
  • Options: rows, maxlength, placeholder, class_control (e.g., markdown, fw-html-editor), attrs_control.
  • Markdown editor preview disables raw HTML by default. For trusted server-controlled or already-sanitized markdown editor fields only, set attrs_control to include data-markdown-trusted="1" or add markdown-trusted to class_control.
  • Common sample:
{
  "type": "textarea",
  "field": "idesc",
  "label": "Description",
  "rows": 5
}
  • Full sample with HTML editor class:
{
  "type": "textarea",
  "field": "idesc2",
  "label": "Rich Text",
  "rows": 10,
  "maxlength": 2000,
  "class_control": "fw-html-editor",
  "help_text": "Requires /common/html_editor script"
}

type: email

  • Template: /common/form/showform/email.html.
  • Options: required, maxlength, placeholder, validate (typically exists isemail), class_control, attrs_control.
  • Common sample:
{
  "type": "email",
  "field": "email",
  "label": "Email",
  "required": true
}
  • Full sample with validation hints:
{
  "type": "email",
  "field": "contact_email",
  "label": "Contact Email",
  "maxlength": 128,
  "validate": "exists isemail",
  "class_control": "on-refresh",
  "help_text": "Unique per record; validated server-side"
}

type: number

  • Template: /common/form/showform/number.html.
  • Options: min, max, step, maxlength, placeholder, required, validate (isfloat), class_control, attrs_control.
  • Common sample:
{
  "type": "number",
  "field": "qty",
  "label": "Quantity",
  "min": 0,
  "step": 1
}
  • Full sample with validation:
{
  "type": "number",
  "field": "rating",
  "label": "Rating",
  "min": 0,
  "max": 10,
  "step": 0.5,
  "validate": "isfloat",
  "placeholder": "0 - 10",
  "class_control": "w-25"
}

type: range

  • Template: /common/form/showform/range.html; show/view dispatch uses the same template and disables the control in view contexts.
  • Options: min, max, step, required, class_control, attrs_control, help_text.
  • Range uses the standard form content width by default. Leave class_contents unset or use "col" when the slider should fill the available row.
  • Use for approximate bounded numeric values such as scores, percentages, priorities, and thresholds. Use number when the user must type an exact quantity, money amount, id, or precise measurement.
  • Common sample:
{
  "type": "range",
  "field": "priority_score",
  "label": "Priority",
  "min": 0,
  "max": 100,
  "step": 5
}
  • Full sample with helper text:
{
  "type": "range",
  "field": "confidence",
  "label": "Confidence",
  "min": 0,
  "max": 10,
  "step": 1,
  "help_text": "Approximate score; visible value is shown beside the slider"
}

type: password

  • Template: /common/form/showform/password.html.
  • Options: maxlength, placeholder, required, class_control, attrs_control.
  • Common sample:
{
  "type": "password",
  "field": "pass",
  "label": "Password"
}
  • Full sample with custom autocomplete handling:
{
  "type": "password",
  "field": "new_pass",
  "label": "New Password",
  "maxlength": 128,
  "placeholder": "leave blank to keep current",
  "attrs_control": "autocomplete=\"new-password\""
}

type: currency

  • Template: /common/form/showform/currency.html.
  • Options: currency_symbol, maxlength, placeholder, required, class_control, attrs_control.
  • Common sample:
{
  "type": "currency",
  "field": "price",
  "label": "Price",
  "currency_symbol": "$"
}
  • Full sample with helper text:
{
  "type": "currency",
  "field": "budget",
  "label": "Budget",
  "currency_symbol": "€",
  "maxlength": 12,
  "placeholder": "0.00",
  "class_control": "text-end",
  "help_text": "Displayed and posted as plain number with currency symbol"
}

type: autocomplete

  • Template: /common/form/showform/autocomplete.html.
  • Options: autocomplete_url (required), lookup_model/lookup_field, lookup_by_value (store text instead of id), admin_url, required, maxlength, placeholder, class_control, attrs_control, prepend/append.
  • Common sample:
{
  "type": "autocomplete",
  "field": "dict_link_auto_id",
  "label": "DemoDicts Autocomplete",
  "autocomplete_url": "/Admin/DemoDicts/(Autocomplete)?q=",
  "lookup_model": "DemoDicts",
  "lookup_field": "iname"
}
  • Full sample storing typed value and adding a helper button:
{
  "type": "autocomplete",
  "field": "city",
  "label": "City",
  "autocomplete_url": "/Admin/Cities/(Autocomplete)?q=",
  "lookup_model": "Cities",
  "lookup_field": "iname",
  "lookup_by_value": true,
  "placeholder": "Type to search or enter custom city",
  "append": [
    {
      "label": "Manage",
      "class": "btn-outline-secondary",
      "icon": "bi bi-box-arrow-up-right",
      "url": "/Admin/Cities"
    }
  ]
}

type: multicb

  • Template: /common/form/showform/multi.html.
  • Options: EITHER lookup_model (stores comma-separated ids in the same table field) OR model (uses a junction model). Add is_by_linked for junctions where the main id is stored on the linked side; lookup_params can tune lookup queries.
  • Lookup-model multicheckboxes use the same active-row rule as selects: active choices are shown, and inactive rows are shown only when they are already checked for the edited field.
  • Common sample storing ids in the same table field:
{
  "type": "multicb",
  "field": "dict_link_multi",
  "label": "DemoDicts Multi",
  "lookup_model": "DemoDicts"
}
  • Full sample using a junction model:
{
  "type": "multicb",
  "field": "demo_dicts_link",
  "label": "DemoDicts via Junction Table",
  "model": "DemosDemoDicts",
  "is_by_linked": false,
  "help_text": "Uses DemosDemoDicts.updateJunction... methods to save"
}

type: multicb_prio

  • Template: /common/form/showform/multi_prio.html.
  • Options: model (required junction model with _link[prio]), is_by_linked to flip main/linked behaviour.
  • Common sample:
{
  "type": "multicb_prio",
  "field": "demo_dicts_prio",
  "label": "Prioritized Categories",
  "model": "DemosDemoDicts"
}
  • Full sample with linked-id mode:
{
  "type": "multicb_prio",
  "field": "user_roles",
  "label": "User Roles (ordered)",
  "model": "UsersRoles",
  "is_by_linked": true,
  "help_text": "Order is saved in _link[prio] from the junction model"
}

type: radio

  • Template: /common/form/showform/radio.html.
  • Options: data via lookup_model, lookup_tpl, or options; is_inline for horizontal layout; class_control and attrs_control pass through to inputs.
  • Common sample:
{
  "type": "radio",
  "field": "status",
  "label": "Status",
  "lookup_tpl": "/common/sel/status.sel",
  "is_inline": true
}
  • Full sample using lookup_model:
{
  "type": "radio",
  "field": "access_level",
  "label": "Access Level",
  "lookup_model": "AccessLevels",
  "lookup_field": "iname",
  "class_contents": "col-md-6",
  "help_text": "Inline radios sourced from AccessLevels model"
}

type: yesno

  • Template: /common/form/showform/yesno.html.
  • Options: is_inline, plus layout keys.
  • Common sample:
{
  "type": "yesno",
  "field": "is_active",
  "label": "Active",
  "is_inline": true
}
  • Full sample with hint:
{
  "type": "yesno",
  "field": "is_archived",
  "label": "Archived?",
  "is_inline": false,
  "help_text": "Stored as 0/1"
}

type: cb

  • Template: /common/form/showform/cb.html.
  • Options: layout keys plus attrs_control (for data-*), default checked when value is truthy.
  • Common sample:
{
  "type": "cb",
  "field": "is_active",
  "label": "Active"
}
  • Full sample with helper text:
{
  "type": "cb",
  "field": "is_checkbox",
  "label": "Email Opt-in",
  "attrs_control": "data-noautosave=\"true\"",
  "help_text": "Posted as 1 when checked"
}

type: switch

  • Template: /common/form/showform/switch.html; show/view dispatch uses the same template and disables the control in view contexts.
  • Options: layout keys, required, class_control, attrs_control, and help_text.
  • Save behavior: same as checkbox fields. Add the field to save_fields_checkboxes so unchecked switches save the configured default, usually 0.
  • Use for reversible binary settings. Keep cb for list membership or acknowledgement checkboxes, and use yesno when the user must make an explicit yes/no choice.
  • Common sample:
{
  "type": "switch",
  "field": "is_enabled",
  "label": "Enabled"
}
  • Full sample with checkbox save default:
{
  "save_fields_checkboxes": {
    "is_enabled": "0"
  },
  "showform_fields": [
    {
      "type": "switch",
      "field": "is_enabled",
      "label": "Enabled",
      "help_text": "Posted as 1 when switched on"
    }
  ]
}

type: date_popup

  • Template: /common/form/showform/date_popup.html.
  • Options: required, class_control, attrs_control (pass-through to input), plus layout keys.
  • Save behavior: normalizes the submitted value to SQL YYYY-MM-DD. Use this for date-only fields even when the DB column is datetime.
  • Common sample:
{
  "type": "date_popup",
  "field": "due_date",
  "label": "Due Date"
}
  • Full sample with custom class and hint:
{
  "type": "date_popup",
  "field": "expires_on",
  "label": "Expires On",
  "class_control": "on-refresh",
  "attrs_control": "data-noautosave=\"true\"",
  "help_text": "Uses bootstrap-datepicker"
}

type: date_combo

  • Template: /common/form/showform/date_combo.html.
  • Options: class_control (applies to all three selects), plus layout keys.
  • Save behavior: normalizes the combined value to SQL YYYY-MM-DD.
  • Common sample:
{
  "type": "date_combo",
  "field": "dob",
  "label": "Birth Date"
}
  • Full sample with helper text:
{
  "type": "date_combo",
  "field": "start_on",
  "label": "Start On",
  "class_control": "form-select-sm",
  "help_text": "Converts to a single date on save"
}

type: datetime_popup

  • Template: /common/form/showform/datetime_popup.html.
  • Options: default_time (preselects time part), class_control, attrs_control, plus layout keys.
  • Save behavior: keeps datetime semantics and converts between fw.userTimezone and UTC.
  • Backing DB notes: use ordinary datetime/datetime2 for DB-local wall-time storage normalized by the framework, _utc field names for UTC instants that must not be shifted through the DB timezone, and SQL Server datetimeoffset when the stored value must retain an offset-aware instant.
  • Common sample:
{
  "type": "datetime_popup",
  "field": "start_time",
  "label": "Start",
  "default_time": "09:00"
}
  • Full sample with placeholder control:
{
  "type": "datetime_popup",
  "field": "scheduled_at",
  "label": "Scheduled At",
  "default_time": "now",
  "class_control": "on-refresh",
  "attrs_control": "data-noautosave=\"true\"",
  "help_text": "Stores combined date and time"
}

type: datetime_local

  • Template: /common/form/showform/datetime_local.html.
  • Options: class_control, attrs_control, plus layout keys.
  • Save behavior: uses browser-native yyyy-MM-ddTHH:mm input and converts that user-local wall time between fw.userTimezone and UTC.
  • Backing DB notes: same as datetime_popup; _utc suffix and SQL Server datetimeoffset rules still come from the field name/type.
  • Common sample:
{
  "type": "datetime_local",
  "field": "starts_at",
  "label": "Starts At"
}

type: time

  • Template: /common/form/showform/time.html.
  • Options: min, max, step, required, class_control, attrs_control. On save, values are converted to seconds; set conv: "time_from_seconds" in the Show configuration to display seconds as HH:mm:ss.
  • Common sample:
{
  "type": "time",
  "field": "start_at",
  "label": "Start Time"
}
  • Full sample for second-based storage:
{
  "type": "time",
  "field": "duration",
  "label": "Duration",
  "min": "00:00",
  "max": "12:00",
  "step": "00:05",
  "help_text": "Saved as seconds; pair with conv:\"time_from_seconds\" on Show"
}

type: att_edit

  • Template: /common/form/showform/att.html.
  • Options: att_category (modal filter, defaults to general), att_post_prefix (hidden input prefix, defaults to field name), attrs_control, plus layout keys.
  • Common sample:
{
  "type": "att_edit",
  "field": "photo",
  "label": "Photo",
  "att_category": "general"
}
  • Full sample with custom prefix:
{
  "type": "att_edit",
  "field": "avatar_id",
  "label": "Avatar",
  "att_category": "photos",
  "att_post_prefix": "att_photo",
  "help_text": "Uses Att modal to select/upload"
}

type: att_links_edit

  • Template: /common/form/showform/att_links.html.
  • Options: att_category (defaults to general), att_post_prefix (defaults to att), plus layout keys.
  • Common sample:
{
  "type": "att_links_edit",
  "field": "_att_links",
  "label": "Documents"
}
  • Full sample with prefixed inputs:
{
  "type": "att_links_edit",
  "field": "_att_links_images",
  "label": "Images",
  "att_category": "images",
  "att_post_prefix": "att_images",
  "help_text": "Selected ids are posted under att_images[...]"
}

type: att_files_edit

  • Template: /common/form/showform/att_files.html.
  • Options: att_category (filter uploads/listing), att_post_prefix (defaults to field name), att_upload_url (defaults to /Controller/(SaveAttFiles)/{id}), multiple, fwentity (entity code to auto-create in Att), plus layout keys.
  • Common sample:
{
  "type": "att_files_edit",
  "field": "_att_files",
  "label": "Files",
  "att_category": "general",
  "multiple": true
}
  • Full sample with custom endpoint and prefix:
{
  "type": "att_files_edit",
  "field": "_att_files_docs",
  "label": "Documents",
  "att_category": "docs",
  "att_post_prefix": "att_docs",
  "att_upload_url": "/Admin/Demos/(SaveAttFiles)/{id}",
  "fwentity": "demos",
  "multiple": true,
  "help_text": "Skips PATCH updates when the post prefix is absent"
}

type: subtable_edit

  • Template: /common/form/showform/subtable.html.
  • Options: model (required), save_fields (space-separated fields persisted per row), save_fields_checkboxes (checkbox defaults such as is_active|0), required_fields (per-row validation), plus any model-specific keys like related_field_name or lookup_params.
  • Common sample:
{
  "type": "subtable_edit",
  "field": "demos_items",
  "label": "Subtable",
  "model": "DemosItems",
  "save_fields": "iname qty",
  "save_fields_checkboxes": "is_checkbox|0"
}
  • Full sample with validation and main-id override:
{
  "type": "subtable_edit",
  "field": "line_items",
  "label": "Line Items",
  "model": "OrdersItems",
  "related_field_name": "orders_id",
  "save_fields": "product_id qty price",
  "save_fields_checkboxes": "is_taxable|0 is_gift|0",
  "required_fields": "product_id qty price",
  "help_text": "Rows are validated and saved via OrdersItems model"
}

form_tabs

form_tabs allows organising large forms into multiple tabs. When more than one tab is present, the template /common/form/tabs.html renders the navigation.

Configuration example:

{
  "form_tabs": [
    {
      "tab": "",
      "label": "Default"
    },
    {
      "tab": "general",
      "label": "General"
    },
    {
      "tab": "advanced",
      "label": "Advanced"
    }
  ],
  "showform_fields": [
    // default tab form fields
  ],
  "showform_fields_general": [
    {
      "field": "iname",
      "type": "input",
      "label": "Title"
    }
  ],
  "showform_fields_advanced": [
    {
      "field": "idesc",
      "type": "textarea",
      "label": "Description"
    }
  ]
}

Each entry defines the tab code (tab) and the text shown on the tab (label). Fields for a tab should be placed in show_fields_TAB and showform_fields_TAB arrays where TAB is the value from form_tabs. If only one tab is defined the tab bar is hidden. Active tab is set by tab parameter in the URL, e.g. /Admin/DemosDynamic/123?tab=advanced. If no tab is specified, the default tab is active.

Form issues

Base, Dynamic, and Vue controllers share one request-owned fw.FormIssues collection. Only two severities are supported: error blocks saving; warning provides feedback while allowing saving. Controller helpers supply the structured entries:

addFormError("email", "EMAIL");
addFormError("title", message: "Check this value.");
addFormWarning("title", message: "Review this value before continuing.");
// For code that selects the severity dynamically:
addFormIssue(FW.ISSUE_WARNING, "title", message: "Review this value.");

Use FW.ISSUE_ERROR and FW.ISSUE_WARNING when a severity value is needed. Each entry has severity, field, and a translated or application-provided message; optional code, tab, row_id, and value carry validation metadata. An empty field represents a form-wide issue. Adding the same severity/field/row again replaces that entry; an error and warning may coexist for the same field.

fw.getFormErrors() derives a fresh field/code map from errors only. Required fields project as true; the REQUIRED or INVALID summary flag supports existing message templates. Warnings never appear in this map. JSON error.details remains the supported generic error-details contract, including explicit application-provided details. It is not limited to form validation.

Standard save responses include form_issues when there are issues. Validation failures return HTTP 400 with error.details and structured errors; warning-only saves remain successful. Dynamic and Vue saves check errors before persistence. Plain controllers/custom actions must still call validateCheckResult() before writing. A false validation result without a specific error adds a form-wide error. Authorization and unrelated server exceptions keep their separate handling.

Dynamic and classic forms

The existing data-errors attribute accepts either the field/code map or the structured array. Existing template bindings stay valid; opt into structured feedback on the initial HTML render by changing only the value:

data-errors="<~error[details] json>"
<!-- or, for errors and warnings with messages: -->
data-errors="<~form_issues json>"

fw.process_form_errors(form, payload) accepts both shapes and replaces feedback only inside that form. Standard autosave uses response.form_issues when present and otherwise uses response.error.details. Errors retain the danger toast and invalid-field highlighting. Coded errors use the existing matching err-CODE template when available, preserving field-specific translations and application customizations; otherwise the escaped structured message is shown. For a custom message that overrides a template, add the issue with message: and no code. Warnings appear as non-blocking field text; an error takes precedence when the same field also has a warning. Successful ordinary HTML posts retain the warning flash message across the redirect. Custom AJAX/modal handlers can pass response.form_issues ?? response.error?.details to the same helper.

There is no additional Dynamic summary, tab navigation, or save coordinator. The renderer matches the exact input name or item[field], including posted subtable keys. Form-wide errors use the existing save error/toast rather than manufacturing a field. Apps can customize the renderer or consume the array themselves.

Both /Admin/DemosDynamic and /Admin/DemosVue demonstrate structured feedback. With otherwise valid fields, set Title to validation-error to block saving, validation-warning to allow saving with a title warning, or validation-both to show a title error and an email warning together (the error blocks saving). Other titles use ordinary validation. Warning messages describe what needs review without claiming that a save succeeded.

Vue presentation

Vue consumes form_issues and also accepts field errors from error.details. Failed saves show a danger toast; repeated identical failures on the same form/tab do not repeat the toast until that tab succeeds. Field validation uses the existing save-status badge and failed-tab indicators, without a duplicate unsaved row or generic top danger alert. Transport, authorization, and unrelated server failures remain separate save alerts. Failed saves retain entered form values and do not reload or navigate the list. A failed tab keeps its error even if another tab saves successfully; success navigation resumes only after each failed tab saves successfully. Manual screen changes ask before leaving a failed form; cancelling preserves the draft. Browser history cancellation restores its current URL, and hard navigation uses the browser unsaved-change warning.

Set fwStoreState.uioptions.edit.is_validation_summary to true to show a compact, wrapping danger summary (warning styling when only warnings exist). It defaults to false in the shared store and is enabled in DemosVue for demonstration. Summary buttons include field labels and select the tab, open containing fieldsets, and focus the control. Customize common/vue/form-issues.html for a vertical list or another presentation. fwStoreActions.issueLabel(issue) can customize labels; subtable summaries use "Subtable Field: Message" (using the configured subtable and child labels when available). Row identifiers stay internal for focusing the matching row; the child field name is the fallback when no label is defined.

Vue resolves subtable controls by component name subtable_<field>. Register application-specific components in the controller's index/vue_components.html. The example belongs to admin/demosvue/index/vue/subtable_demos_items.html; the common virtual-controller bundle does not load demo-specific components.

The demo subtable uses a single anchored tooltip per invalid cell, visible on hover or focus, with the invalid control styling retained. Tooltips do not increase row height; inputs reference their messages with aria-describedby. Custom scrollable subtables should check clipping and overlap when adopting this presentation. Bootstrap validation tooltips have accessibility limitations; applications can retain inline feedback where appropriate.

For Dynamic and Vue, an optional attempted value is included only for a field definition with JSON boolean validation_show_value: true. Leave that setting absent for sensitive values. Password and hidden controls do not expose attempted values. Plain controllers have no field definitions: callers must ensure any explicitly supplied attempted_value is safe to return. Messages themselves must also avoid sensitive data; the framework cannot determine which application text is confidential.

Built-in validation text lives in common/form/validation-messages.sel and uses the current template language (lang/<language>.txt). Initial HTML supplies the same translated messages to the Vue store for error codes and inline errors. Custom issue messages remain application-provided text; translate them before passing them to the helpers. The issue summary labels also use template language markers.

Repeated-row issues use the exact posted field key, for example item-lines#22[quantity], and row_id: "22". A custom subtable field wrapper should provide matching data-fw-field and data-fw-row attributes and tabindex="-1". If it uses the common form group, pass def.issue_field with the posted key and form.row_id with the row id. fwStore.fieldIssues(def, form) provides matching messages. The supplied editable subtable examples demonstrate the same contract.

Migrating C# form validation

FW.FormErrors has been replaced by FW.FormIssues; there is no second mutable error dictionary to synchronize. Update application writers and readers:

Previous code Replacement
fw.FormErrors["email"] = "EMAIL"; addFormError("email", "EMAIL");
fw.FormErrors["title"] = true; addFormError("title", "REQUIRED");
fw.FormErrors["title"] = "Custom message"; addFormError("title", message: "Custom message");
Read fw.FormErrors Read fw.getFormErrors() for the field/code map, or fw.FormIssues for structured entries
fw.FormErrors.Clear(); fw.FormIssues.Clear(); (clears both errors and warnings)

Remove explicit REQUIRED/INVALID marker writes; the projection derives these flags. Use validateCheckResult() to enforce blocking validation instead of treating any non-empty issue collection as failure. The optional dictionary parameter of validateRequired() still collects local errors without adding request-wide issues. Re-rendered controllers share the request collection; initializing another controller does not clear it. The small prepareSaveFields() helper shares standard Dynamic/Vue validation and field filtering; authorization and persistence remain in the action flow.

Vue interaction behavior

The shared behavior is exported from wwwroot/assets/js/vue-interactions.js and imported by the common Vue templates. Copy that asset and the updated site.css alongside the templates; application fwStoreActions overrides still take precedence.

Saved column widths

Vue list headers support dragging the right-edge resize control, pressing Left/Right for ten-pixel steps, Home/End for the limits, and double-clicking to fit visible text. Ctrl+double-click fits all visible data columns in one save, using the current page's content and preserving hidden-column widths. The eight-pixel grab area shows a one-pixel divider on hover or keyboard focus, with a resize cursor. Widths are rounded and limited to 60–800 pixels. The user_views.widths JSON map stores up to 100 configured columns, alongside the existing fields and density. Unknown columns and invalid widths are discarded. Resizing saves the current default view; saving a named view copies the current widths, loading a view restores them, and resetting a view clears them. Hidden configured columns retain their widths; removed columns are ignored.

Resizing one column preserves the current rendered widths of the other columns, including after Reset to Defaults. Only explicitly resized or auto-fitted data columns receive saved widths. Unconfigured columns use their natural layout when a table or view loads. The selection and action columns are not resizable; actions stay on one line with room for configured buttons and slots.

Width, density, reset, and named-view writes run in request order for each store. A pending resize completes before a later reset or view selection, so older width requests cannot restore widths after the reset. Failed width writes retain the last confirmed widths without replacing a newer resize.

Apply the provider's additive upd2026-09-08-user-view-widths.sql update before copying these model/controller changes into an existing application. Fresh SQL Server, SQLite, and MySQL schemas include the column. The saved-view action still requires POST and the current XSS token, and existing owner/system authorization remains in force.

For custom persistence, override fwStoreActions.saveColumnWidths(changes), where changes maps field names to widths. Both batch fitting and the standard saveColumnWidth(field, width) action use it; existing single-column overrides still apply to single-column gestures.

Action availability and fields that are read-only on edit

Initial Vue state contains capabilities: { create, edit, delete }. It reflects controller/user restrictions. Per-row _capabilities can further restrict actions; it cannot widen the controller's permissions. getCapabilities() and getListRowCapabilities(row) are the server extension points. Read-only rows and virtual controllers participate in the same shape. Common action controls use this metadata, while actual actions retain server authorization checks. Missing metadata in an older custom frontend retains its previous defaults.

Set is_edit_readonly: true on a form field to allow entry when creating a row and display its value without an editable control afterward. Existing-row updates ignore submitted changes to that field, including forged requests; ordinary full-form submissions remain usable. New rows retain the field. Editable subtables can declare child fields in their nested showform_fields, using the same metadata. Custom subtable controls must also honor those child definitions for presentation. This metadata governs the standard controller save paths; model writes and custom actions must enforce any application-wide immutability requirement separately.

Both /Admin/DemosDynamic and /Admin/DemosVue demonstrate this with the existing Code (icode) field, without an additional database column. Open Add New to enter a Code, then edit an existing record to see its read-only value. Both states show the help text "Set when creating the record; read-only when editing."

Filter visibility and quick edit

The filter panel starts open and remembers its visibility in browser storage. Keys include origin, application/controller URL, list/edit mode, and related-record context. Hiding the panel preserves its filter values. If storage is unavailable, the panel remains usable with an open default. A small bump toggle above the panel's upper-right edge stays in place when collapsed without reserving vertical space in either state; its tooltip and accessible label switch between "Hide filters" and "Show filters". The toggle is part of list-filters, so custom screen templates should keep that component mounted and let it manage its form's visibility.

Quick-edit context retention is opt-in (is_quick_edit_keep_context replaces the unreleased quick_edit_keep_context spelling):

"store": {
  "is_quick_edit_keep_context": true
}

After a successful quick edit, the client fetches current list rows, count, paging, filtering and ordering from the server while preserving the open pane, input focus and selection. The server remains responsible for whether an edited row still matches the list. A refresh failure reports that the save succeeded but the list needs reloading. Existing save triggers remain unchanged; this option adds no new autosave trigger. Identical simultaneous saves of the same form are suppressed, while newer requests for the same form and tab are coalesced into one followup. Explicitly requested tabs retain their first-request order; delayed autosaves keep the tab where they originated. Requests for different forms are tracked separately. Reconciliation uses only subtables processed for the requested tab, preserves child edits, additions and removals made after a request starts, and applies assigned IDs before a queued save. Navigation alone does not request a save; newer unsaved edits prevent a response from reloading or leaving their form. Explicit failed deletions do not clear selection or reload the list.