Skip to content

About

Widget for data entry in IronFlock dashboards

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

widget-form

A Lit 3.x web component for creating dynamic forms with configurable fields. Part of the IronFlock widget ecosystem.

screenshot

Installation

npm i @ironflock/widget-form

Usage

<script type="module">
    import '@ironflock/widget-form'
</script>

<widget-form-1.0.9></widget-form-1.0.9>

Note: The version number is part of the custom element tag name.

Properties

Property Type Description
inputData FormConfiguration Form configuration and fields
theme Theme Theme object for styling
route string The board page's path with its ?query, set by IronFlock Studio on boards

Events

Event Detail Description
data-submit Array of field/value mappings Fired when the form is submitted
file-data-submit { items, waitUntil } (see File Fields) Fired instead of data-submit when the save includes a new file
nav-submit { path } After Delete, with the resolved deleteNavigationRoute
param-submit { params: { [openParameter]: undefined } } When a dialog opened by a URL parameter closes (see Open on URL Parameter)

Configuration

The form is configured via the inputData property:

interface FormConfiguration {
    title?: string
    subTitle?: string
    formButton?: boolean // Show button to open form dialog
    autoSubmit?: boolean // Save on Change: no Reset or Submit, inline forms only
    formButtonStyle?: FormButtonStyle // How that button looks
    openParameter?: string // The URL parameter that opens that dialog instead of the button
    formDialogSize?: FormDialogSize // Minimum size of that dialog
    fieldLayout?: FieldLayout // How the fields share rows (see Layout)
    formFields?: FormField[]
}

Form Open Button

With formButton: true the tile shows only a button that opens the form in a dialog. formButtonStyle sets how it looks:

interface FormButtonStyle {
    fab?: boolean // Floating action button (default); false for a regular button
    label?: string // Label Text of the regular button
    size?: 'small' | 'medium' | 'large'
    cornerRadius?: number // Corner radius in px, for both kinds of button
    bgColor?: string // Background colour
    color?: string // Icon colour, and label colour on the regular button
    iconName?: string // Material icon name, 'add' by default
}

By default (fab unset or true) it is a round Material floating action button that shows only the icon, 40, 56 or 96px by size. With fab: false it is a Material filled button with the icon and label (only the icon when label is empty), 32, 40 or 56px tall by size; a long label is cut with an ellipsis at the tile's edge. Unset, its label and icon are black or white, whichever reads on its background.

cornerRadius rounds the corners of either button, in px: 0 is square, and half the button's height or more is fully round (a circle for the floating action button, a pill for the regular button). Unset, the floating action button keeps Material's 12, 16 or 28px corners by size, and the regular button is a pill.

Every field except fab can be bound to a column, so the button can follow data: a fault state can turn it red, swap its icon to warning and relabel it "Report fault". A bound value that does not fit falls back to the default: a size other than small, medium or large gives medium, an invalid colour the theme's, a corner radius that is not a number of 0 or more (numeric text counts) Material's corners, and an empty icon name add. A numeric label is shown as it is.

Save on Change

With autoSubmit: true (and no formButton) the form has no Reset or Submit button: every change saves the whole form, as Submit would. A checkbox, dropdown or picked colour saves at once; text, numbers and dates save when the field loses focus or on Enter. An invalid field blocks the save and shows its error, as Submit does. After a save the fields keep what was entered rather than going back to their pre-filled values, so bind those to the stored row and they follow it. A Delete button stays, and "Saving…" shows while a file is stored. For a single on/off setting, widget-switch with a targetColumn is the better fit: it shows the stored position and writes a row per click.

Dialog Size

With formButton: true the form opens in a dialog that is 480px wide (narrower on small screens) and as tall as its fields, up to 80% of the window height. formDialogSize sets the dialog's minimum width and height:

interface FormDialogSize {
    minWidth?: string // e.g. '800px', '60%', '50vw'
    minHeight?: string // e.g. '600px', '70%', '90vh'
}

Each value is a number with an optional unit: px (assumed when there is none), % or vw/vh for a share of the window, rem or em. Empty keeps the default. A value replaces Material's own 280×140px minimum and is capped at the window less 24px on each side, so the dialog and its Submit button always stay on screen; a form taller than that scrolls inside the dialog. On an ordinary window a value below the default size therefore changes nothing, while on a window narrower than 328px (or a very short one) any value, even a large one, lets the dialog shrink a little below Material's minimum to keep those margins. Anything else, such as auto, calc() or a negative number, is ignored with a console warning. Fields that take part of a row (see Layout) fit more of them on a row in a wider dialog.

Open on URL Parameter

With formButton: true and openParameter set to a parameter name, such as edit, the form is an edit dialog that another widget opens: on a board the tile shows no button, and the dialog is open while the page URL carries the parameter (?edit=press-1). A widget-table column of type edit sets it to the clicked row's key; so can a link. Closing the dialog (Cancel, Submit, Delete) removes the parameter again, so the same row can open it again, and the browser's Back button closes it as well.

The record comes through the fields' pre-filled values, bound in Studio to the record's latest row filtered by the same parameter: a Parameter filter named edit plus the Latest switch, on each bound field, and the record's key column as a hidden field with the same binding, so a save writes the same entity. While Studio re-queries those bindings after the parameter changed, the dialog shows a progress bar and its Save, Delete and Reset buttons are disabled; the fields then rebuild from the record.

# board-template.yml (Studio stores the same)
openParameter: $str:edit
formFields:
    - label: $str:Machine # the entity key, written back unchanged
      hiddenField: $bool:true
      targetColumn: { tablename: machines, column: machine_id, dataType: string }
      preFilledValue:
          valueRef:
              tablename: machines
              column: machine_id
              queryParams:
                  limit: 1
                  filterAnd:
                      - { latest: true, column: '', operator: '=', value: '', dataType: boolean }
                      - { column: machine_id, operator: '=', value: edit, dataType: string, useFilter: true }
    - label: $str:Name # every edited field: the same filter, its own column
      type: $str:textfield
      targetColumn: { tablename: machines, column: name, dataType: string }
      preFilledValue: { valueRef: { tablename: machines, column: name, queryParams: { limit: 1, filterAnd: [...] } } }

Keep a "New" form as a tile of its own: with the parameter absent, Studio drops the filter and the bindings would deliver the newest record. A Delete with deleteNavigationRoute drops the parameter in the same navigation. Outside a board (the Widget Editor's preview, where there is no route) the button stays, so the form can be tried; a bookmarked URL with the parameter opens the dialog on load.

Dialog Colours

The dialog and its dropdown menus take the tile's colour as it is displayed (the tile over the dashboard background), so the text colour reads on them. On a dark tile they are lightened one step and the backdrop behind the dialog darkens more (60% instead of 32%), so the dialog stands out from the page; on a light tile they keep the tile's colour.

Field Types

Each form field supports the following types:

Type Description
textfield Single-line text input
numberfield Numeric input with min/max
checkbox Boolean checkbox
textarea Multi-line text input
dropdown Select from predefined options
datetime Date/time picker
file Upload one file (FleetFiles)
secret Masked password or token
color Color picker, stored as hex
section Heading over a group of fields

Field Configuration

interface FormField {
    label: string
    type: 'dropdown' | 'textfield' | 'numberfield' | 'checkbox' | 'textarea' | 'datetime' | 'file' | 'secret' | 'color' | 'section'
    width?: FieldWidth // Share of the row; fieldLayout.defaultWidth when unset
    newRow?: boolean // Start a new row even if the field would fit on the current one
    groupWidth?: FieldWidth // For a section: the width of its group's fields
    hiddenField?: boolean // Hide field but still submit value
    required?: boolean // Field must be filled
    description?: string // Hint text shown below field
    targetColumn?: TargetColumn // Database column mapping
    preFilledValue?: string // For a file: the record's current key; not for a secret
    defaultValue?: string
    preFilledValueColor?: string // For color, in place of preFilledValue: a color picker in the editor
    defaultValueColor?: string // For color, in place of defaultValue
    min?: number // For numberfield
    max?: number // For numberfield
    validation?: string // Regex for textfield and secret (a secret's minimum requirements)
    validationMessage?: string // For textfield and secret: shown when the regex fails
    browserPasswordManager?: boolean // For secret: let the browser save and fill it (default off)
    values?: DropdownValue[] // For dropdown type
    fileNamespace?: string // For file: the app's FleetFiles namespace (required)
    acceptedFileTypes?: string // For file: 'application/pdf, image/*'
    maxFileSizeMB?: number // For file: 1 MB = 1,048,576 bytes
    fileNameColumn?: TargetColumn // For file: gets the original file name
    fileSizeColumn?: TargetColumn // For file: gets the size in bytes
    fileTypeColumn?: TargetColumn // For file: gets the content type
    fileUrlColumn?: TargetColumn // For file: gets the file's permanent URL (Studio writes it)
}

Layout

Fields fill the form left to right in rows, in the order of formFields. By default every field takes a full row at Material's standard size, which is the single column of earlier versions.

interface FieldLayout {
    defaultWidth?: FieldWidth // For fields without a width of their own or their section's; 'full' when unset
    minFieldWidth?: number // In px; 180 when unset, 0 never widens a field
    density?: 'comfortable' | 'compact' // 'comfortable' when unset; new forms start 'compact'
}
type FieldWidth = 'full' | 'three-quarters' | 'two-thirds' | 'half' | 'third' | 'quarter'
  • A field's width, else its section's groupWidth, else fieldLayout.defaultWidth, sets its share of the row. A field that does not fit on the current row starts the next one, and newRow: true starts a new row even when it would fit.
  • In a form too narrow for its fields, they widen so that none is narrower than minFieldWidth. Below room for four, quarters become thirds and three quarters become two thirds. Below room for three, every field takes half a row, and below room for two, a full row. A three-quarters and a quarter field, or a two-thirds and a third field, therefore stay on one row while there are two columns. The form measures its own width, so this follows the tile and the dialog; the default 480px dialog fits two fields on a row.
  • Number, date and color fields on a full row keep their 200px. On part of a row they fill it.
  • A dropdown's label sits inside its outline, as on the text fields, so the two line up side by side. A file field's caption sits above its box, so put a file field on a row of its own or beside another file field.
  • A checkbox on part of a row is centered in the height of the text fields beside it.

density: 'compact' makes the form smaller for desktop boards used with a mouse, and is what a form dropped on a board starts with (the default config sets it). Unset, a form is comfortable, Material's size, which is how forms made before stay. Fields are 40px high instead of Material's 56px, with 14px text, the buttons are 32px high, and the gaps between fields and rows shrink. The Form Open Button keeps its own size.

In a tile (without formButton), the Delete, Reset and Submit buttons stay at the bottom while the fields scroll above them, with a thin line between the two while more fields are hidden below. A short form keeps its buttons right under its last field. In a tile with less than about 160px left under its title, the fields and the buttons scroll together.

Sections

An entry of type section is a heading, not an input. It shows its label, with its description under it, across a full row, and a line above it unless it comes first. It groups the fields after it, up to the next section, and stores nothing. Its groupWidth sets the width of the fields in its group that have no width of their own.

A section's conditionalDisplay shows or hides the heading and its whole group. The fields of a hidden group store NULL, as any field hidden by Conditional Display does, while a hiddenField in it keeps sending its value. Keep the controlling field outside the group it controls: once the group is hidden, a field inside it reads as its default value. A section never controls a condition, so it may share its label with a field.

File Fields

A file field stores one file in the app's file storage (FleetFiles) and writes the stored object's key into its target column, a string column, in the same save as the rest of the form.

  • The field is a drop zone: click it to pick a file or drop one on it. It shows the file's name and size, and a button to remove it. Reset clears it.
  • acceptedFileTypes (MIME types, type/* allowed) filters the picker and refuses other files on the spot; so does maxFileSizeMB. The browser's type is used when it fits, otherwise the one the file's extension stands for, so a CSV that Windows calls application/vnd.ms-excel is still text/csv.
  • fileNamespace names a namespace from the files section of the app's data template. Its own contentTypes and maxObjectBytes apply as well. Files up to 6 MB upload on every installation; larger ones only where the platform can issue upload links (the cloud, not an appliance).
  • fileUrlColumn gets the stored file's permanent platform URL, so other widgets can show or link the file: an embed of a PDF, an image. Opening it needs a signed-in IronFlock session. Studio builds it after the upload; it stays empty where the installation serves no file links, and with a Studio version that does not write it.
  • In an edit form, bind preFilledValue to the record's key column. The field then shows that file (the key's last segment is the file name), and a save without a new file writes the key back unchanged. Without a pre-filled key and without a new file, the column is left out of the row, never set to null, so a table with entity keys keeps its previous value. The name, size, type and URL columns are only written with a new file. A file field hidden by Conditional Display submits null, like every other field.

The widget never uploads anything itself. A save with a new file dispatches file-data-submit instead of data-submit:

detail: {
    items: Array<
        | { swarm_app_databackend_key, table_name, column_name, value } // as in data-submit
        | {
              swarm_app_databackend_key, table_name, column_name, file: File, namespace: string, contentType: string
              urlColumn?: { swarm_app_databackend_key, table_name, column_name } // the field's fileUrlColumn
          }
    >
    waitUntil(outcome: Promise<unknown>): void
}

The host claims the event by calling waitUntil() while it is dispatched. It uploads each file under a key of its choosing, puts the key in as that item's value, writes the object's permanent URL into the item's urlColumn (null where it cannot build one), and writes the rows only when every upload succeeded. A null item for the URL column comes ahead of the file item, and a later item for the same column wins: a host that writes no URL leaves the column empty instead of keeping another file's URL. The form shows "Saving…" until the promise settles. If it resolves, the form resets and the dialog closes. If it rejects, the form keeps its values and shows the error's message. A host that does not know the event (Studio before file support) never claims it, and the form says that it cannot upload and saves nothing, rather than sending a File through data-submit, which would write garbage into the column.

Color Fields

A color field takes a color for a string column, such as the color a chart or a status lamp shows for a category. It is a text field with a swatch: click the swatch, or tab to it and press Enter or Space, for the browser's color picker, or type or paste any CSS color (#1e88e5, 1e88e5, rgb(30, 136, 229), teal).

  • It stores lowercase hex, #rrggbb, or #rrggbbaa for a translucent color, which every widget and chart library reads. A typed color is shown as that hex once the field loses focus. The browser's picker has no transparency, so picking keeps the alpha of a translucent color typed before.
  • Text that is no color marks the field invalid, and the form cannot be saved until it is fixed. A pre-filled value that is no color is shown as it is and, unless the user changes it, written back unchanged.
  • Left empty, the field stores its default value, which its swatch shows, or else NULL. Required means it cannot be left empty when there is no default or pre-filled value, as for the other fields.
  • Set preFilledValueColor and defaultValueColor, for which the editor shows a color picker; preFilledValue and defaultValue are read when they are empty.

Secret Fields

A secret field takes a password, token or key without showing it: the input is masked, and an eye button shows what was typed. It is write-only: a saved secret never comes back to the widget, so the field has no pre-filled value and always starts empty.

  • Point it at a string column declared secret: true in the app's data template. FleetDB encrypts that column and never returns the value to a board (Studio shows ••••••••); only the app's own containers can read it, with the SDK's revealSecrets(). Any other column stores the value as plain text.
  • Left empty, the field leaves its column out of the save. A table that keeps the latest row per entity (maintainLatestFlagFor) then carries the stored secret over; any other table stores NULL. The widget does not send FleetDB's keep sentinel (__secret__): on a table without entity keys FleetDB rejects the whole row, and Studio's publish does not report that, and a column that is not secret: true would store it literally.
  • browserPasswordManager decides whether the browser treats the field as a password. Off (the default), the field is a text input drawn as dots (-webkit-text-security), so neither the browser nor a password manager extension offers to save, fill or generate anything, which suits a PIN; autocomplete="off" keeps the value out of the browser's form history, phones do not capitalise or correct it, and copying is blocked while it is masked. On, it is a password input marked autocomplete="new-password": the browser may offer a strong password and to save it, but does not fill in the viewer's IronFlock login.
  • required means a new value must be typed on every save, so leave it off in a form that edits existing records. validation sets the minimum requirements for a newly typed value, such as ^.{12,}$ for at least 12 characters or (?=.*[0-9])(?=.*[A-Za-z]).{10,} for 10 with a letter and a digit; the whole value must match. Set validationMessage to say what is required ("At least 10 characters, with a letter and a digit"): it replaces the browser's generic "Please match the requested format", which tells nothing about a value one cannot see. Repeat it in description to show it before anyone types.
  • The value is sent exactly as typed, spaces included.

Development

# Start dev server
npm run start

# Build for production
npm run build

# Generate types from schema
npm run types

# A release pull request; its merge publishes to npm
just release-patch

License

MIT

About

Widget for data entry in IronFlock dashboards

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages