FForm Platform
enzh-CN

Form controls reference

Detailed behavior, contracts and implementation guidance for FormPlatform input, layout, media and visualization controls.

Pagination component

Purpose

`Pagination` is a reusable multi-step form container. It owns page selection, per-page validation, error summaries, and Previous / Next / Submit navigation. It does **not** own a database provider, a survey deployment, or a server action. The surrounding `FormReader` host receives a normal submit event and decides persistence.

The same component can therefore be used by surveys, ordinary Data Model forms, functional forms, or an Action Code-controlled workflow.

Flow

1. **Previous** moves to the preceding visible page without validation.

2. **Next** evaluates the current page's validation mode: block, none, or validate-but-continue.

3. When Next may continue, it optionally persists the current form state, then advances.

4. **Submit** validates all visible pages, runs `onSubmit`, then performs the configured final completion action.

Visible page headers are progress navigation, not a validation bypass. A user can return to an already reached page; a future page header remains disabled until Next reaches it.

General settings

SettingMeaning
Show page headersShows headers in Preview and Viewer. Designer always shows them so pages remain editable.
Arrange page headers verticallyUses a left-side page rail. Disabled uses horizontal, wrapping headers.
Submit data when clicking NextEnables a save after the page may continue.
Next-page submission mode`Standard form submission` emits a normal submit. `Survey progress save` emits `completionStatus: in_progress`.
Final-page completion modeSelects the action after full validation.

Final completion choices are:

  • **Standard form submission**: normal validated FormReader submit; use for System, Data Model, and Functional forms.
  • **Survey submission (Completed)**: validated submit with `completionStatus: completed`; use for deployed surveys.
  • **Run Submit event only**: no automatic save; the `onSubmit` handler owns the workflow.
  • **Automatic (legacy compatibility)**: preserves schemas created before these settings existed.

New Pagination controls default to standard final submission and do not save on Next. Existing schemas that lack these properties preserve their former survey-compatible behaviour.

Events tab

EventWhen it runsBoundary
`onPrevious`Before Previous changes pageNo validation.
`onNext`When current page may continueDoes not run when block validation fails. It can receive `valid: false` for validate-but-continue.
`onSubmit`After all visible pages validateRuns before automatic final submission.

Each handler receives the usual Form Action context and `event.detail`:

{
  pageId: 'page_2',
  pageTitle: 'Contact details',
  pageIndex: 1,       // zero based
  pageNumber: 2,      // one based
  isLastPage: false,
  valid: true,
  validationMode: 'block'
}

`onSubmit` also receives `completionMode`. A synchronous global handler may return `false` to cancel the automatic follow-up action. Async Form Action module functions run asynchronously and must handle their own failure result.

Examples

Ordinary business form

Select **Standard form submission** and leave Next saving disabled. The last page uses the normal Form Viewer save route and the configured submit-button redirect.

Deployed survey

Enable Next saving, choose **Survey progress save**, and select **Survey submission (Completed)** for the last page. The survey host receives `in_progress` on intermediate saves and `completed` on final submission.

Action-controlled completion

Select **Run Submit event only**, then enable `onSubmit` in Events with `completeRegistration`:

export default {
  async completeRegistration({ api, event, data }) {
    const result = await api.fetch('/api/registration/complete', {
      method: 'POST',
      body: JSON.stringify({ data, page: event.detail.pageNumber })
    })
    api.notify({ type: 'success', message: result.message || 'Completed.' })
    await api.navigate('/registration/thank-you')
  }
}

To use ordinary FormPlatform persistence from Action Code instead, call:

api.reader.submit({ redirectUrl: '/registration/thank-you' })

It validates through FormReader once more, then emits a normal host submission.

Limits

  • Pagination validates only controls currently visible under normal form rules. Hidden controls are not submitted by FormReader.
  • Navigation events belong to the Pagination component; the three virtual buttons are not separately persisted Button controls. Configure appearance in General and behaviour in Events.
  • Survey completion status is meaningful only when interpreted by a survey-deployment host. In a normal viewer it is an unused submission option.

---

Calendar Component and Chinese Almanac Calculations

中文:CALENDAR_COMPONENT.zh-CN.md

Architecture and data flow

`FormCalendar.vue` remains a normal `value` / `update:value` form control. Its date-only value is a Gregorian civil string (`YYYY-MM-DD`); optional time remains the existing local `YYYY-MM-DD HH:mm` presentation contract. The month grid passes numeric civil year, month, and day values to `lunarCalendar.js`. That pure module returns the lunar label, solar-term label, and stem-branch values; it does not parse an ISO instant or read browser timezone state.

`LUNAR_INFO` is the sole 1900–2100 lunar-year data source. Bits `0x8000` through `0x10` encode the twelve regular month lengths, the low nibble identifies the leap month, and bit `0x10000` encodes the leap-month length. Conversion starts at the known 1900-01-31 / lunar 1900-01-01 epoch and subtracts those encoded year and month lengths. It does not infer alternating month sizes and does not maintain a second Chinese New Year or leap-month table.

Solar terms are defined by apparent geocentric solar longitude at 15-degree intervals. For each term, the implementation numerically solves the longitude crossing with a compact astronomical solar model and then converts that instant to a UTC+8 civil date using Julian-day arithmetic. The four 1901–2100 cases that the Hong Kong Observatory identifies as exceptionally close to midnight use explicit published civil-date overrides, preventing minute-scale model uncertainty from selecting the adjacent day. No browser timezone or fixed interval between terms participates in the result. The year pillar switches on the civil date of 立春; the month pillar switches only on the twelve 节 (小寒、立春、惊蛰…大雪) and derives its stem from the effective post-立春 year through 五虎遁.

Security boundaries

This is display-only, client-side calendar math. It makes no network request, has no database or API dependency, and does not accept executable input. Form values remain untrusted input at the normal form submission boundary; calendar calculations neither validate nor authorize submissions.

Compatibility and limits

The Vue props, Schema contract, `v-model` event, styles, and navigation behavior are unchanged. The year selector and arrow navigation are bounded to 1900–2100; an already stored out-of-range value is preserved in the input but the almanac footer degrades to dashes. `getSolarTerm(year, month)` retains the footer form and accepts an optional day to label a grid cell. `STEM_BRANCH_YEAR(year)` remains for legacy Gregorian-year callers; date-aware callers use `yearStemBranch(year, month, day)` and `monthStemBranch(year, month, day)`.

The stem-branch footer follows the selected form value when one exists. Before a value is selected, it uses today's civil date while the visible month is the current month; after navigating to another month it uses that month's first day. The control therefore does not silently write today into the form merely to show today's pillars.

Lunar conversion and solar terms return `null` outside 1900–2100 or for invalid civil dates. Dates before 1900-01-31 cannot be represented by the supplied lunar dataset. Day pillars intentionally accept any valid proleptic Gregorian civil date and return `null` only for invalid values, preserving their timezone-independent contract. Year and month pillars require the supported range because their boundaries depend on the supported solar-term calculation.

Validation

Run `npm run test:unit -- src/components/lunarCalendar.test.js` from `src/FormPlatform.Host/ClientApp`, then `npm run build`. The direct tests cover regular month sizes, leap-month start/end, all 24 term dates in 2024, near-midnight term dates in 2021/2051/2083/2084, 立春 and monthly 节 boundaries, cross-century day pillars, and invalid/out-of-range inputs. Expected lunar and solar-term dates are based on the Hong Kong Observatory's Gregorian-Lunar Calendar Conversion Tables (1901–2100): <https://www.hko.gov.hk/en/gts/time/conversion.htm>.

---

Chart components

FormPlatform provides six Chart.js controls: **Bar**, **Line**, **Scatter**, **Doughnut**, **Pie**, and **Radar**. They are read-only data controls: a chart can display static JSON, an API response, or a JSON-valued form/Data Model field. It never changes the bound value itself.

Add and configure a chart

1. In Form Designer, expand **Charts** and drag a chart to the canvas.

2. Open **General** and set its title, title size, legend position, responsive behavior, and height.

3. Select a data source:

- **Static JSON**: JSON held in the schema. Best for a fixed illustration or a small dashboard sample.

- **API**: a same-origin URL returning JSON. The Designer intentionally does not call it; it renders local sample data instead.

- **Form data binding**: the current form value. Set **Other → Property Name** and map it to a JSON Data Model attribute when persistence is required.

4. Choose **Simplified dataset** for one dataset configured from component properties. Clear it to pass native Chart.js `{ labels, datasets }` data unchanged.

Simplified data

For Bar, Line, Doughnut, Pie, and Radar, set **Data labels** and use a numeric array:

[12, 19, 8, 15]
["Q1", "Q2", "Q3", "Q4"]

For Scatter, the simplified value is an array of X/Y points; Data labels are normally empty:

[
  { "x": 4, "y": 12 },
  { "x": 9, "y": 5 },
  { "x": 17, "y": 18 }
]

The Dataset Label and Dataset Background Color properties provide the one dataset's display settings. Background Color accepts a CSS color such as `#2563eb`, `rgba(37,99,235,.7)`, or a JSON color array.

Full Chart.js data

Turn **Simplified dataset** off to supply a native Chart.js data object. This is required for several datasets, dataset-specific options, or advanced colors.

{
  "labels": ["Q1", "Q2", "Q3", "Q4"],
  "datasets": [
    { "label": "Orders", "data": [12, 19, 8, 15], "backgroundColor": "#2563eb" },
    { "label": "Returns", "data": [2, 4, 1, 3], "backgroundColor": "#f59e0b" }
  ]
}

The same object can be stored in a JSON Data Model column and loaded through Form and Data Mapping. Chart controls are recognized by generated-table previews as a JSON field.

API data source

The host includes anonymous example endpoints for testing only:

URLUse
`/api/examples/charts/sales`Simplified numeric data: `{ "data": [12,19,8,15] }`
`/api/examples/charts/sales-full`Full Bar/Line/Radar Chart.js data
`/api/examples/charts/scatter`Simplified scatter points
`/api/examples/charts/scatter-full`Full scatter Chart.js data

For a production module, return either the direct chart value or wrap it in `{ "data": ... }`. A minimal endpoint looks like this:

api.MapGet("/monthly-sales", async (IOrderReport report, CancellationToken ct) =>
{
    var rows = await report.MonthlySalesAsync(ct);
    return Results.Ok(new
    {
        labels = rows.Select(x => x.MonthName),
        datasets = new[]
        {
            new { label = "Sales", data = rows.Select(x => x.Total), backgroundColor = "#2563eb" }
        }
    });
}).RequireAuthorization();

Do not expose an unrestricted reporting endpoint merely because a chart is visible. Apply the module's authorization and validate all query parameters on the server.

Form data binding

Use form binding when the chart should render a JSON field already loaded with the record. For example:

1. Add a `Line chart`, choose **Form data binding**, clear **Simplified dataset**, and set Other → Property Name to `monthlySales`.

2. Add a JSON attribute named `monthlySales` to the data model and map it as readable.

3. Store the full Chart.js data object in that field.

The chart is read-only. Use a DataGrid, editable table, actions, or a dedicated input form to edit the data source.

Responsive layout

Each chart observes its rendered FormPlatform container, including flex rows, tab/page changes, GridLayout panes, and collapsing side panels. With **Responsive** enabled (the default), Chart.js reflows its drawing area to the available width. The control itself always uses `min-width: 0`, `max-width: 100%`, and an overflow boundary, so it cannot widen a form row beyond its parent.

For small screens, use a moderate fixed **Height**, keep legends at `top` or `bottom`, and avoid very long category labels. Chart.js automatically skips category ticks when appropriate. Do not set a fixed width through the Style Tab unless the enclosing container has the same responsive constraint.

Bar width

Bar charts provide **Bar width (px)** in the General tab. `0` (or an empty value) leaves sizing to Chart.js, which is the recommended responsive default. A positive value fixes the pixel width of every bar dataset, including datasets supplied in the full Chart.js data format. Use a modest width when the chart can become narrow; an unnecessarily large fixed value may cause bars to overlap or become clipped when there are many labels.

Installation note

The client now depends on `chart.js`. Run `npm install` once in `src/FormPlatform.Host/ClientApp` to update `package-lock.json`; then use the usual `npm run build` and host build/publish process. `npm ci` requires the updated lock file.

---

Rich Text Editor control

`Rich Text Editor` is a form data control for short articles, formatted notes, instructions, terms, and other HTML text entered by a user. It is available in the Designer **Controls** panel and is independent of the CMS module.

What is stored

The value is a string containing safe HTML, for example:

{
  "articleBody": "<h2>Welcome</h2><p>Please read the <a href=\"/terms\">terms</a>.</p><ul><li>First item</li></ul>"
}

Set **Other → Property Name** to `articleBody` (or the target column/property name). The control then follows the ordinary form-data path:

  • a mapped form writes the value to a string attribute through the ORM;
  • a generic submission stores it in the submission data JSON;
  • a survey stores it in the generated response table;
  • a generated form table creates a text/string column for it.

For a relational data model use a sufficiently large text field: PostgreSQL `text`, SQL Server `nvarchar(max)`, or MySQL `longtext`. Do not map it to a short `varchar` unless the business rule deliberately limits the document length.

Designer configuration

1. Drag **Rich Text Editor** from **Controls** onto the form.

2. In **General**, set the label, label position, placeholder, minimum height, toolbar visibility, read-only/disabled state, and change debounce timeout.

3. In **Other**, set the Property Name, default value, required rule, validation, visible/read-only conditions, and events just as for other data controls.

4. When using a Data Model, add a string attribute with the same Property Name and create or update the form mapping.

The initial editor value is empty. Use **Other → Default value** only when a real default document is intended; it will be submitted like any other default form value.

Editing experience

The built-in toolbar offers bold, italic, underline, paragraph, heading, bulleted list, numbered list, quote, link, and clear-formatting operations. The control supports the standard `onChange`, `onFocus`, and `onBlur` events. `onChange timeout` coalesces rapid edits before the action chain is invoked.

In read-only mode the same sanitized HTML is rendered as formatted content. This makes the control suitable for both edit and view forms.

Safety model

Rich text is treated as **data**, never as executable markup. Both browser and server apply the same small allow-list before the value is rendered or persisted.

Allowed elements are paragraphs, line breaks, headings, emphasis, underline/strikethrough, lists, block quotes, code/preformatted text, and links. An anchor retains only a safe `href`: `http`, `https`, `mailto`, root-relative URLs, and hash links.

Scripts, event attributes, styles, embedded frames, forms, SVG/MathML, images, arbitrary classes, and dangerous URL schemes are removed. Therefore this control intentionally does **not** accept Tailwind classes or custom HTML layout. Use normal FormPlatform containers, Style Tab settings, CMS blocks, or a purpose-built custom component for layout and media.

The server-side sanitizer runs before mapped entity saves, generic submissions, survey submissions, and assisted survey submissions. Code that accepts raw HTML through a custom module/API must still regard that input as untrusted and should call `FormRichTextSanitizer.SanitizeHtml(value)` when it wants this policy.

Example form fragment

{
  "id": "articleBodyControl",
  "type": "richText",
  "props": {
    "label": "Article body",
    "placeholder": "Write the announcement…",
    "minHeight": 280,
    "toolbar": true,
    "fluid": true
  },
  "other": {
    "propertyName": "articleBody",
    "required": true,
    "validationTrigger": "blurAndDebouncedInput"
  }
}

Deliberate boundaries

  • It is not a CMS page builder and does not replace structured CMS blocks.
  • It does not upload images or files. Use the File Upload control or shared media service and place an approved link in the text if needed.
  • It does not promise exact Word/Google Docs formatting after paste; unsupported formatting is removed on purpose.
  • It is not a collaborative editor. Concurrent users follow the same last-write/version rules as other form fields.

---

Signature and Camera controls

`Signature` and `Camera` are first-class, image-producing form data controls. They use the existing FormPlatform media services instead of placing image bytes in the submitted form record.

Add a control

In Designer, open **Input controls** and drag either **Signature** or **Camera** into the form. Assign a unique **Property name** on the **Other** tab when the control participates in a Data Model mapping, generated table, generic submission record, or survey response table.

Both controls have the standard required, custom validation, readonly, visibility, styling and `onChange` settings.

Signature

The respondent draws on a pointer-enabled canvas (mouse, pen, or touch). The canvas creates a PNG only when **Upload signature** is selected.

  • **Clear canvas** removes the unsaved drawing.
  • **Cancel** discards the unsaved drawing.
  • **Upload signature** uploads a PNG and puts its returned media reference into the form value.

Clear and Cancel intentionally do not delete an already-saved signature. Uploading a replacement changes the form field value; normal storage retention/cleanup policy governs an older unreferenced asset.

The General tab configures canvas height, pen color, background color, and stroke width.

Camera

**Open camera** opens a focused dialog first. Once Vue has mounted the dialog and its `<video>` preview element, the control asks the browser for a stream and waits for the preview to become playable. This mirrors the historical Camera control's modal lifecycle and avoids attaching a native stream to an element that does not yet exist. If the request fails, the dialog keeps the diagnostic message and provides **Retry camera**. The control can request the front or rear camera, capture a JPEG, or accept a local image using **Choose from device**. A selected or captured image is only persisted after **Upload photo**.

Camera access requires HTTPS (or `localhost`) and a browser permission grant. Camera requests the configured front/rear facing mode first, then falls back once to bare `video: true` when the preferred profile cannot start. The dialog also lists the browser-visible cameras. Choose a concrete device when a desktop has more than one webcam, virtual camera, or capture device; that explicit selection is requested by `deviceId` and is retained only in that browser for that control. It is never stored in the form definition, because browser device IDs are origin-specific and can change. Select **Automatically select a camera** to return to the normal front/rear preference.

A transient driver-start failure receives one short delayed retry rather than repeatedly opening every visible device; this is intentionally conservative because rapid native open attempts can keep some Windows camera drivers in a `NotReadableError` state. Browsers intentionally do not reveal which application holds a camera; if it continues to fail, close other browser tabs, conferencing tools, native camera applications, and then retry. Local file selection remains available when the camera is unavailable or the user declines permission.

Only one FormPlatform Camera control may hold or be opening a browser video stream at a time. Opening another Camera control on the same page invalidates the earlier FormPlatform request or releases its live stream. A browser request cannot be cancelled directly, so a late stream from an invalidated request is stopped as soon as it resolves. This avoids hidden, repeated, or nested form renderers contending for a single physical camera.

Image upload mode

The General tab provides an image-specific upload mode:

ModeBehaviour
AutoSurvey deployment: survey attachment. Other saved forms: FormPlatform shared media.
Platform form shared mediaStores in the normal form-media service. Use this for ordinary mapped, generic, or functional forms.
Survey attachmentStores in `survey_files`; use only in a deployed survey.
Third-party custom APISends the image to the configured API URL. The API must return either an ID string or an object containing an `id`. Return `downloadUrl` as well when the control must show the uploaded image immediately.

For shared media, choose **Private** or **Public** visibility. A normal form must already be saved before it can upload shared media. Designer deliberately never uploads images.

Persistence and mappings

The field value is a media reference object while the browser is running; normal relational mapping stores its stable `id` in a string column. A form-generated table therefore creates a string field for Signature and Camera. In a manually designed Data Model, use a string/varchar attribute (normally length 36 for the current FormPlatform ID convention).

Survey responses store the attachment ID in the response table. When the response is loaded, FormPlatform resolves it back to a download reference so both controls show the saved image.

Security boundary

The standard `/api/forms/{formId}/media` endpoint verifies that the requested control is a Signature or Camera control configured for form/shared-media upload. It does not accept an arbitrary control ID. Survey upload retains its deployment, form and respondent ownership checks. Custom APIs are owned by the form/module author and must implement their own authorization and validation.

Example schema fragment

{
  "id": "customerSignature",
  "type": "signature",
  "props": {
    "label": "Customer signature",
    "height": 180,
    "penColor": "#0f172a",
    "backgroundColor": "#ffffff",
    "strokeWidth": 2,
    "imageUploadMode": "auto",
    "imageVisibility": "private"
  },
  "other": { "propertyName": "customer_signature", "required": true }
}

Use `type: "camera"` with `preferredCamera: "environment"` and `imageQuality: 0.9` for a photo field.

---

FileUpload control

`FileUpload` is the standard file-data control for ordinary forms and survey forms. It is separate from the legacy `Input` control with `type = file`: it supports an upload queue, multiple selections, drag-and-drop, configurable file rows, image thumbnails, and persisted platform media references.

Add it to a form

1. In Designer, drag **File upload** from the data controls panel.

2. Set its **Property name**. This is the form-data property and, when the form is mapped, the mapped attribute name.

3. In **General**, choose the presentation and storage properties described below.

4. Save the form before testing a platform-managed upload. The form ID is required to authorize and own a shared-media upload.

When a file is uploaded through FormPlatform, the form value is a media reference, not file bytes. A single-file control produces one reference; a multiple-file control produces an array of references. This keeps the response/form table small and lets the media service enforce download authorization.

General properties

PropertyMeaning
Label / label positionStandard data-control label.
Button textText of the picker button when **Use DropZone** is off.
Accepted file typesBrowser `accept` hint, for example `.pdf,image/*`. It improves selection UX but is not a security policy. The server/media provider remains authoritative.
Image file types for iconsComma-separated filename patterns, such as `*.png,*.jpg,*.gif`. Used to choose an image icon/preview when the MIME type is unavailable.
Show file type iconShows a compact file-type icon for each selected or uploaded item.
Auto process queueUploads selected files immediately. When off, files remain queued until the user clicks **Upload queue**.
Use DropZoneReplaces the picker button with a clickable drag-and-drop target. It accepts the same files and options.
Show image previewShows thumbnails for image items where the browser has a local preview or the provider returned a download URL.
Allow safe previewAdds a separate **Preview** link for images, PDF, plain text, CSV, and JSON. It opens in a new tab. HTML, Office documents, executables, and unknown types are never previewed by this control.
Allow multiple choiceEnables browser multiple selection and stores an array of references. Generated database tables use a JSON column for this value.
Read only / DisabledStandard controls. Read-only still displays existing uploaded items and download links; neither state permits selecting/removing files.
ID fieldField used as the identity when a custom upload API returns an object. Use `id` for FormPlatform media. Nested paths are accepted, for example `result.fileId`.

File properties

The File properties table controls how each uploaded item is presented; it does not duplicate the file into the form record.

  • **File property** is the supplied logical row (`name`, `length`, `contentType`, or `token`).
  • **Column title** is the display label.
  • **Table field** is the property read from a returned media-reference object, such as `fileName`, `fileSize`, `contentType`, or `id`.
  • **Show property** enables the row. Use only `Name` for a compact attachment list, or add `Length` and `Content type` for a detailed list.

Style areas

The normal **Style** tab settings still control the FileUpload component wrapper. FileUpload additionally exposes semantic internal areas in the same tab, so styles do not depend on fragile DOM selectors:

AreaElement styled
LabelThe top, left, or right FileUpload label.
File frameThe bordered file-control frame.
Picker / queue buttonThe file-picker button and the queued upload/clear buttons.
DropZoneThe drag-and-drop selection area.
File listThe container for saved and queued rows.
File itemEach saved or queued file row.
File informationThe flexible filename/metadata section of each row.
File actionPreview, download, and clear actions.
ThumbnailAn image thumbnail when image preview is enabled.
Queue actionsThe manual queue upload/clear strip.

Enter Tailwind utilities or a custom CSS class for each area, for example `rounded-xl border-slate-300 shadow-sm` for **File frame** or `bg-blue-600 text-white` for **Picker / queue button**. These fields are stored with `*Class` names, so their Tailwind candidates are collected when the form schema is saved. The component intentionally retains these semantic structural elements; removing them would make list layout and per-area customization less reliable.

Storage modes

ModeUseResult
AutoDefaultIn a survey deployment, uploads to survey attachments. In any other saved form, uploads to FormPlatform shared form media.
Platform form shared mediaNormal secured form attachmentUploads through the form-media endpoint. Private media can only be downloaded through authorized form access; public media may use the public media URL.
Survey attachmentDeployed surveyUploads through the survey attachment endpoint and is validated against the deployment, form, and respondent when submitted.
Third-party custom APIModule/provider-owned filesSends the selected file to the configured API URL. The API should return either an ID string or an object containing the configured ID field plus optional `fileName`, `fileSize`, `contentType`, and `downloadUrl`.
Manual (Action Code)Custom browser workflowDoes not upload. The selected `File` (or `File[]` for multiple mode) is available through the form runtime file API for Action Code to process. The ordinary submitted value is the filename unless Action Code replaces it.

For FormPlatform-managed modes, **Media visibility** is `private` by default. Choose `public` only for files that are safe to expose outside normal form authorization.

Download and preview behavior

Every **Download** link sent by the built-in FileUpload control requests `Content-Disposition: attachment`, so the browser downloads the file instead of opening it inline. This includes images, PDFs, and text files.

**Preview** is a separate, opt-in control property. It only appears for a conservative allow-list of safe document types and opens a new tab without giving that page access to the opener. A custom upload API owns its download URL and may choose its own behavior; FormPlatform does not append its download parameter to an external URL.

Data Model mapping and table generation

  • Map a single-file Property name to a nullable string/varchar attribute. The stored value is the media ID/reference ID.
  • Map a multiple-file Property name to a JSON attribute. For tables generated from the form, the generator selects JSON automatically when **Allow multiple choice** is enabled.
  • The control is a normal data control: required validation checks that at least one uploaded reference exists, and `onChange`, `onFocus`, and `onBlur` work through the normal event chain.
  • For surveys, the response table persists file IDs. When loading a response, the server resolves IDs to download-reference objects only after verifying the current respondent or administrative access.

Changing **Allow multiple choice** changes the response column shape. Do this before a deployment has collected data. Survey tables with data are intentionally not rebuilt automatically.

Custom API example

Configure **Third-party custom API** and set Upload API URL to `/api/acme/documents`. A compatible response can be:

{
  "id": "9c601af0-3b28-4f18-a8b1-6f6136d9d2b9",
  "fileName": "proposal.pdf",
  "fileSize": 128004,
  "contentType": "application/pdf",
  "downloadUrl": "/api/acme/documents/9c601af0-3b28-4f18-a8b1-6f6136d9d2b9/download"
}

The custom endpoint must authenticate and authorize the caller itself. It must validate file size/content, store the file outside the form-data record, and return only the metadata necessary for the control.

Security notes

  • `accept` and filename extension checks are user-interface aids, not trust boundaries.
  • Platform form media endpoint authorization is limited to a `FileUpload` control present in the saved form schema and configured for `auto` or `form` storage.
  • Survey attachment IDs are revalidated on submission; a user cannot submit another respondent's file by posting an arbitrary ID.
  • Do not make uploaded documents public unless the business case explicitly requires public download.
  • Do not use file preview for untrusted HTML or Office documents. Keep **Allow safe preview** off unless the form needs it.

Legacy Input file control

Existing forms using `Input` with `type = file` continue to work. Use `FileUpload` for all new multi-file, queue, dropzone, thumbnail, or configurable file-row scenarios.

---

CustomBlock control

`CustomBlock` is a container that can either own normal child controls or render a reusable component tree from a saved form or JSON. It is useful for reusable presentation fragments such as a standard notice, a shared static navigation section, or a predefined component layout.

Source types

Source typeBehavior
Drop controlsDefault container mode. Drag, sort, and configure children directly inside the block. The children are saved in the current form schema.
Referenced formLoads `schema.components` from the saved form identified by **Form Name**. The source is rendered read-only inside the current form.
JSON sourceParses a component array, `{ "components": [...] }`, or `{ "schema": { "components": [...] } }`, and renders it read-only.

Referenced form authorization

Referenced forms are loaded through the existing `GET /api/forms/by-name/{name}` API. The current user must have read access to the referenced form. This is deliberate: embedding a form cannot bypass Form Access rules.

The API is authenticated, so use **JSON source** or regular local children for an anonymous/public page unless the relevant source is provided through an authorized application flow.

Designer behavior

  • In **Drop controls** mode, CustomBlock is a normal editable Designer container.
  • In **Referenced form** and **JSON source** modes, the source is a preview-only reusable fragment. Editing it from the parent would silently create a copy and make ownership unclear, so edit the referenced form or JSON instead.
  • Invalid JSON and unavailable/unauthorized referenced forms render a visible error rather than silently showing an empty block.

JSON example

[
  {
    "id": "welcome-title",
    "type": "header",
    "props": { "content": "Welcome", "size": 2 }
  },
  {
    "id": "welcome-note",
    "type": "staticContent",
    "props": { "value": "This section is supplied by CustomBlock." }
  }
]

Nested referenced blocks are limited to six levels. This prevents a form from indirectly referencing itself forever.

---

SystemValue / ReadOnlyData control

`SystemValue` is a data control whose displayed value is read-only and whose authoritative value is written by FormPlatform immediately before persistence. It is intended for audit and request context fields such as submission time and authenticated identity.

The control participates in ordinary Data Model mappings, generic submission records, survey deployments, and ACL-authorized system-user assisted survey entry. It is deliberately not validated as an input field.

Add and configure

1. Drag **System value** from the control panel.

2. In **Other**, set a unique **Property Name**, for example `submitted_at`.

3. In **General**, select the source and capture mode.

4. When using a Data Model, map that property to an appropriate attribute. Use a calculated/default database column only when the database, rather than FormPlatform, should own the value.

On a new record, Viewer, deployed-survey, and saved-form designer preview pages request one short-lived server display snapshot and show the real current server time, request IP, identity, and so on. This snapshot is never trusted: immediately before the actual write, the server evaluates every SystemValue again and overwrites the browser payload. A loaded record displays its stored value. A new or not-yet-saved SystemValue control retains the non-authoritative *Recorded by the server when saved* placeholder because the server has no saved form definition to evaluate.

When viewing or editing an existing record, a SystemValue configured for **Update only** or **Create and update** keeps its stored value in the main output and also shows a blue **Will update to on next save** line. That second value is a fresh server preview and is never inserted into the browser submission model. This makes the saved and prospective values explicit rather than making an unsaved update look persisted.

Sources

SourceStored valueNotes
Server timeISO UTC, date, Unix seconds, or a custom .NET formatUse `always` for `updated_at`; use `insert` for `submitted_at`. Custom formats are stored as text.
Client IP addressResolved remote IPConfigure forwarded headers correctly behind IIS/Nginx. IPv4 and IPv6 are preserved.
Current system user ID / nameCurrent authenticated system principalEmpty for anonymous and respondent-only requests.
Current respondent IDSurvey respondent idUseful on a deployed survey; empty on normal system forms.
Browser User-AgentRequest user-agent headerInformational; clients can change it.
Request pathPublic request pathQuery string is intentionally excluded.
Request trace IDASP.NET Core trace identifierUseful for correlating a response with server logs.
Browser time zoneBrowser supplied IANA zoneInformational only; the browser sends it in `X-FormPlatform-Time-Zone`.
ConstantConfigured textServer still overwrites the field, preventing payload substitution.

Capture modes

ModeEffect
Create onlyWrites on an insert, retaining the previously stored value on update.
Update onlyWrites only when an existing record is updated.
Create and updateRewrites on every save.

Recommended audit fields

Property nameSourceCapture modeSuggested data type
`submitted_at`Server time / ISOCreate only`DateTimeOffset` / timestamp with time zone
`updated_at`Server time / ISOCreate and update`DateTimeOffset` / timestamp with time zone
`submitted_by`Current system user IDCreate onlyGuid / native UUID
`updated_by`Current system user IDCreate and updateGuid / native UUID
`respondent_id`Current respondent IDCreate onlyGuid / native UUID
`client_ip`Client IP addressCreate and updatestring / varchar(45)
`request_id`Request trace IDCreate and updatestring

For survey deployment table generation, SystemValue server-time fields generate a date, 64-bit integer, or `DateTimeOffset` field for the built-in formats. A custom time format and other sources generate text fields.

Use **Custom format** when the presentation or integration requires a fixed string, for example `yyyy-MM-dd HH:mm:ss`. It uses the .NET `DateTimeOffset` custom format syntax and always formats UTC server time.

Trust model

The submitted JSON is cloned, then SystemValue properties are overwritten on the server before triggers, Server Action dispatch, generic persistence, Data Model persistence, or survey persistence. Editing browser devtools or posting a forged JSON value cannot replace a server-owned source.

`browserTimeZone` and `userAgent` describe the client but are not security claims. `clientIp` is reliable only when the reverse proxy forwarding policy is restricted to trusted proxies; otherwise an untrusted forwarded header must never be trusted.

Example form fragment

{
  "id": "comp_submitted_at",
  "type": "systemValue",
  "props": {
    "label": "Submitted at",
    "labelPosition": "top",
    "source": "serverTime",
    "captureMode": "insert",
    "timeFormat": "iso",
    "fluid": true
  },
  "other": { "propertyName": "submitted_at" }
}

Do not mark a SystemValue as required. The server owns its value and assigns it at the persistence boundary.

---

Semantic Control Styling Standard

Purpose

Form definitions must be able to style every visible, meaningful part of a control without relying on fragile DOM selectors. This is the required design standard for every new FormPlatform control.

Contract

1. `style.customClass` styles the control's outer wrapper.

2. A control with multiple visible regions exposes one named object, for example `fileUploadStyles`, `chartStyles`, or `selectionStyles`.

3. Each member ends in `Class` and names a semantic region, never an implementation detail. Examples: `labelClass`, `frameClass`, `toolbarClass`, `optionClass`, `emptyClass`, and `errorClass`.

4. The Vue template applies the semantic class directly to the corresponding DOM element. User classes are appended after the stable semantic class.

5. Built-in component CSS uses `:where(...)` whenever possible, so an authored Tailwind utility can override it without `!important`.

6. Canvas-rendered content is configured through explicit rendering properties (colours, fonts, grid and legend options), because CSS cannot style pixels drawn by Canvas.

Existing control families

FamilyStyle object / existing style surface
Input, Textarea, Rich text, System value`inputStyles`, `textareaStyles`, `richTextStyles`, `systemValueStyles`
Upload and device controls`fileUploadStyles`, direct GeoLocation class fields, `signatureStyles`, `cameraStyles`
Selection controls`selectionStyles` on Dropdown, AsyncSelect, Tree, Radio, Checkbox and Calendar
Display controls`headerStyles`, `imageStyles`, `qrCodeStyles`, plus outer `customClass`
Chart controls`chartStyles` plus chart drawing properties
DataGrid, ItemRenderer, Menu, Message, Statistic, Breadcrumb, Tabs and Pagination`semanticStyles` plus their existing documented column, pane, item, action and layout settings
Table, EditableTable and GridLayout`semanticStyles` for frame, table/pane, header, body, row/cell, empty state and resize handle as applicable; their existing detailed cell/pane/layout settings remain available
Containers and simple content controlsouter `customClass`; children remain independent controls and own their styling

Required implementation checklist for a new control

  • Add a semantic `...Styles` prop with an empty-object default when it has more than one visible region.
  • Add every `...Class` field to the Style tab, defaults in `ComponentPanel.vue` and `store/formDesigner.js`, and the component contract.
  • Add English and Chinese labels in `i18n.js`.
  • Add every class to the element it describes; do not require authors to know nested DOM structure.
  • Keep defaults low-specificity with `:where`.
  • Ensure the schema CSS collector can discover the class fields. The `Class` suffix is intentional.
  • Add a focused component-contract/unit test when the control has interactive state.

Authoring example

For a multiple AsyncSelect, set:

{
  "selectionStyles": {
    "controlClass": "rounded-2xl border-indigo-300 bg-indigo-50",
    "chipClass": "bg-indigo-600 text-white border-indigo-600",
    "panelClass": "rounded-2xl shadow-2xl",
    "selectedOptionClass": "bg-indigo-100 text-indigo-900"
  }
}

This remains stable even if the implementation later changes from a `div` to a native dialog or a virtualized list.