«form» Component Type

📖 Table of Contents

Introduction

In SmarkForm the whole form is a field of the type form which imports and exports JSON data.

👉 The keys of that JSON data correspond to the names of the fields in the form.

👉 From fields can be created over any HTML tag except for actual HTML form elements (<input>, <textarea>, <select>, <button>…) and can contain any number of SmarkForm fields, including nested forms — there is no limit to the nesting depth beyond practical usability.

Value shape and field matching

A form exports a plain object with one property for each named child field. Nested forms export nested objects; lists export arrays. Fields that are empty still appear in the object with the value defined by their type (for example, "" for a text input and null for a number field).

import() accepts a plain object or a JSON string containing an object. It imports matching child names recursively. A child key omitted from the object receives undefined; what that means depends on the child’s import method. Generic input, number, and color fields use it as a reset signal, while the current date/time/datetime-local types normalize it to null and radio groups deselect. Keys with no matching child are ignored. See the relevant type references and Import and Export for the complete value/default contract.

Example:

Following example shows a simple SmarkForm form with two nested forms:

🔗
❓
<div id="myForm">
  <p>
    <label data-smark>Id:</label>
    <input data-smark type='text' name='id' />
  </p>
  <fieldset data-smark='{"type":"form","name":"personalData"}'>
    <legend>Personal Data:</legend>
    <p>
      <label data-smark>Name:</label>
      <input data-smark type='text' name='name' placeholder='Family name'/>
    </p>
    <p>
      <label data-smark>Surname:</label>
      <input data-smark type='text' name='surname' />
    </p>
    <p>
      <label data-smark>Address:</label>
      <input data-smark type='text' name='address' />
    </p>
  </fieldset>
  <fieldset data-smark='{"type":"form","name":"businessData"}'>
    <legend>Business Data:</legend>
    <p>
      <label data-smark>Company Name:</label>
      <input data-smark type='text' name='name' placeholder='Company Name'/>
    </p>
    <p>
      <label data-smark>Address:</label>
      <input data-smark type='text' name='address' />
    </p>
  </fieldset>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "id": "EMP-001",
    "personalData": {
        "name": "Emily",
        "surname": "Watson",
        "address": "456 Oak Avenue, Portland"
    },
    "businessData": {
        "name": "Acme Corp",
        "address": "789 Business Park, Portland"
    }
}
});

👉 The outer form doesn’t need the “data-smark” attribute having it is the the one we passed to the SmarkForm constructor.

ℹ️ In text fields the “name” attribute is naturally taken as field name.

ℹ️ In the case of nested form, having <fieldset> tags cannot have a name attribute, it is provided as a data-smark object property (which is always valid).

🚀 This is a simple showcase form. You can extend it with any valid SmarkForm field.

Clear vs Reset Actions Example:

The following example demonstrates the distinction between clear and reset actions:

🔗
❓
<div id="myForm">
  <fieldset>
    <legend>User Profile (with defaults)</legend>
    <p>
      <label data-smark>Name:</label>
      <input data-smark type='text' name='name' />
    </p>
    <p>
      <label data-smark>Email:</label>
      <input data-smark type='email' name='email' />
    </p>
    <p>
      <label data-smark>Age:</label>
      <input data-smark type='number' name='age' />
    </p>
    <p>
      <button data-smark-action="clear">Clear All</button>
      <button data-smark-action="reset">Reset to Defaults</button>
      <button data-smark-action="export">Show Data</button>
    </p>
    <pre data-export aria-live="polite">Exported data appears here.</pre>
  </fieldset>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "name": "John Doe",
    "email": "john@example.com",
    "age": "30" // Intentional wrong type (see notes)
}
});// Render exported data in the example instead of opening a modal dialog.
const output = myForm.targetNode.querySelector("[data-export]");
myForm.on("AfterAction_export", (ev) => {
    output.textContent = JSON.stringify(ev.data, null, 2);
});

👉 The example has a form-level demoValue; its child controls do not declare their own HTML or field-level defaults.

🔘 Clear All — Imports the form’s type-level empty object; with the child fields in this example, their empty-string values appear.

🔄 Reset to Defaults - Restores the original default values.

💡 Try this sequence:

  1. Modify some field values
  2. Click “Clear All” — the example’s text fields become empty
  3. Click “Reset to Defaults” - default values are restored

ℹ️ The value option on the form sets the default values that reset will restore.

🕵️ Notice the value passed as default for the age field (in the ⚙️ JS tab) is a string (for a nuber field) but SmarkForm’s number field correctly sanitizes it and it is correctly exported as a number afterwards.

API Reference

Options

value

Sets the initial default data for the form. These defaults are restored when reset() is called and are updated by import() (unless set_default: false is passed).

  • Type: Object
  • Default value: {}

See: Setting defaults via value for details on how defaults work, clear vs reset, and the VALUE_CONFLICT error.

focus_on_click

Make forms get focused when clicked anywhere inside them.

  • Type: Boolean
  • Default value: true

auto_id

Automatically generate id attributes for all components in the form. When enabled, each component receives an id derived from its path (e.g. "address_street").

  • Type: Boolean, String, or Function
  • Default value: false
  • false — No id attributes are generated.
  • true — IDs are generated from the component’s path.
  • "prefix-" — Same as true, but prepends the string (e.g. "prefix-address_street").
  • function(path, component) { … } — Custom generator. Receives the component’s path and the component instance.

The updateId() method can be called later to regenerate IDs (e.g. after adding list items).

Actions

Actions are special component methods that can be triggered both programmatically (by directly calling the function) or by user interaction with a trigger component. Each action is associated with a specific behavior that can be performed on the component.

👉 Actions receive an options object as an argument, which contains the necessary information to perform the action.

👉 When called from triggers, the properties defined in their data-smark object, are passed as options to the action. Those expecting a path(★ ) to other components are resolved to the actual component.

The form component type supports the following actions:

(Async) export (Action)

Options (export)
  • action: (= “export”)
  • origin: Origin is automatically set to the trigger component that called the action (and cannot be overridden). In case of a manual call, it is set to Null.
  • context: Path★ (absolute or relative to its natural context) to the component that is the context of the action. If not provided, the context is set to its natural context.
  • target: Path★ (absolute or relative to its context). Pipes the exported data to the import action of the target component.
  • data:

(Async) import (Action)

Options (import)
  • action: (= “import”)
  • origin: Origin is automatically set to the trigger component that called the action (and cannot be overridden). In case of a manual call, it is set to Null.
  • context: Path★ (absolute or relative to its natural context) to the component that is the context of the action. If not provided, the context is set to its natural context.
  • target: Path★ (absolute or relative to its context). Pipes the imported data from the export action of the target component, overridding the “data” option if specified.
  • data: (JSON)
  • focus: (boolean, default true)
  • set_default: (boolean, default true) — When true (the default), the imported data becomes the new default restored by reset(). Pass false to import data without changing the reset target.

(Async) clear (Action)

clear imports the form’s type-level empty object ({}) without changing the form’s stored default. Each child receives undefined; the resulting state is type-specific. Generic input, number, and color imports use it as a reset signal, while date/time/datetime-local fields become null and radio groups deselect. A nested list also honors min_items; retained minimum rows are reset to their own defaults. See the List reference and the specialized type references.

Unlike reset, clear does not restore the form’s stored default, nor update it. A field-level reset may also vary by type; see the specialized type reference before relying on a field-local default.

Example use case: A “New” button that discards the form-level loaded value. If child-local defaults exist, clear those fields separately when the desired behavior is a completely blank form.

Options (clear)
  • action: (= “clear”)
  • origin: Origin is automatically set to the trigger component that called the action (and cannot be overridden). In case of a manual call, it is set to Null.
  • context: Path★ (absolute or relative to its natural context) to the component that is the context of the action. If not provided, the context is set to its natural context.
  • target:

(Async) reset (Action)

Reverts all fields to their configured default values. The default is initially set by the value option and is updated every time import() is called with set_default: true (the default). If no default has ever been set, fields revert to their type-level empty state.

This action is recursive, applying to all nested forms and lists.

Example use case: A “Reset” button that restores the form to its last loaded state.

Options (reset)
  • action: (= “reset”)
  • origin: Origin is automatically set to the trigger component that called the action (and cannot be overridden). In case of a manual call, it is set to Null.
  • context: Path★ (absolute or relative to its natural context) to the component that is the context of the action. If not provided, the context is set to its natural context.
  • target:

(Async) submit (Action)

Submits the root form using native browser submission semantics enhanced with SmarkForm data.

When called on a nested sub-form, submit always delegates to the root form so that the entire root form is submitted, not just the sub-form’s data.

Native submit interception: When the root SmarkForm wraps an actual <form> HTML element, clicking any native <input type="submit"> / <button type="submit"> will also trigger this action automatically, with the native event.submitter passed through so that HTML5 per-button overrides (e.g. formaction, formmethod) are honoured.

Enter key from non-submit fields does not submit. In SmarkForm, Enter navigates between fields (like Tab). This applies to non-button fields — including non-enhanced elements such as a <select> placed inside the native <form> container for UI purposes. Pressing Enter while a submit button (native or SmarkForm trigger) is focused still submits normally, because the browser fires a click event on the button first.

Events: BeforeAction_submit and AfterAction_submit are emitted on the root form by virtue of the @action decorator — no additional event wiring is required.

Submitter name/value (form-encoded only): For non-JSON encodings, when the submitting element has a name attribute its name/value pair is appended to the flattened entries — matching native browser behaviour. This does not apply to JSON encoding (see below).

The submitter element is always available as options.submitter inside BeforeAction_submit and AfterAction_submit handler callbacks for any custom handling.

Options (submit)
  • action: (= “submit”)
  • origin: Origin is automatically set to the trigger component that called the action (and cannot be overridden). In case of a manual call, it is set to Null.
  • context: Path★ (absolute or relative to its natural context) to the component that is the context of the action. If not provided, the context is set to its natural context.
  • submitter (Element, optional) — DOM element that initiated the submission. When coming from a native submit DOM event this is set automatically. When using a trigger button the trigger’s target node is used. Override explicitly to supply a custom submitter.
Encoding and transport

The effective submission attributes are resolved in priority order (submitter attribute → form attribute → default), matching native HTML5 behaviour:

Attribute Submitter override Form attribute Default
action formaction action location.href
method formmethod method get
enctype formenctype enctype application/x-www-form-urlencoded
target formtarget target _self
novalidate formnovalidate novalidate false

JSON encoding (enctype="application/json"): Data is exported as a JSON object and sent via fetch(). Any HTTP method is supported (GET, POST, PUT, DELETE, PATCH, …). After a successful response:

  • If the response was redirected, the browser navigates to the redirect destination.
  • If the response body is text/html, the current document is replaced with it.
  • Otherwise, nothing happens by default.

JSON encoding is disabled by default as a security measure. You must explicitly opt in by setting enable_json_encoding: true on the root SmarkForm instance:

const myForm = new SmarkForm(document.querySelector('#myForm'), {
    enable_json_encoding: true,
});

Without this option, attempting to submit a form with enctype="application/json" throws a clear error at runtime.

The submitter name/value is not automatically added to the JSON body. For JSON submissions the developer has full control over the payload. Access the submitter element via options.submitter inside a BeforeAction_submit handler to incorporate it as needed (e.g. as a URL parameter or a dedicated JSON field).

JSON submission to a target other than _self is not supported; the target is coerced to _self and a warning is printed to the console.

Non-HTTP action URLs (e.g. mailto:, data:) are not supported with JSON encoding and will throw an error. Use the default form-encoded enctype for non-HTTP action URLs.

Non-JSON encoding (URL-encoded / multipart / plain-text): Form data is flattened into name/value pairs and submitted via a temporary hidden <form> element appended to document.body. Only GET and POST methods are supported; using any other method with a non-JSON enctype throws an error. The temporary form is removed after submission.

Non-HTTP action URLs such as mailto:someone@example.com are fully supported through this path — the browser handles them natively (e.g. opening the email client for mailto: with GET).

When the submitting element has a name attribute its name/value pair is appended to the submitted entries — matching native browser behaviour. Be aware that this adds an entry not present in the SmarkForm export data.

Data flattening options

When using non-JSON encoding, the exported SmarkForm JSON is flattened into string key/value pairs. The following options on the root form control the flattening style:

  • key_style (string, default "bracket") — How nested keys are represented:
    • "bracket" — person[address][city]
    • "dot" — person.address.city
  • array_style (string, default "repeat") — How array items are represented:
    • "repeat" — Same name repeated: tags, tags, tags (native PHP / Rails style)
    • "index" — Indexed: tags[0], tags[1] (or tags.0, tags.1 with key_style:"dot")

These options can be set via data-smark on the root form element:

<form data-smark='{"key_style":"dot","array_style":"index"}' method="post" action="/submit">
  …
</form>

Conditional enable/disable (planned)

Forms do not currently support a disabled state that exports null. A proposed enable action could make a Form or List disableable while retaining its internal value. This would be distinct from clear (type-level empty state) and reset (the saved default); there is no separate null action in the current proposal. See Roadmap — Conditional component enablement.