Showcase

This section provides a series of working examples to demonstrate the capabilities of SmarkForm without diving into code details.

It highlights key features through examples, using short and readable code that prioritizes clarity over UX/semantics. The examples use minimal or no CSS (if any you’ll find it at the CSS tab) to show layout independence.

They go step by step from the most basic form to more advanced and fully featured ones.

πŸ‘‰ If you are eager to to see the full power of SmarkForm in action, you can check the πŸ”— Examples section first.

πŸ‘‰ Nonetheless, if you are impatient to get your hands dirty, the πŸ”— Quick Start is there for you.

πŸ“– Table of Contents

Basics

Just a Form

Fields auto-register from HTML attributes β€” no JavaScript beyond initialization. Try editing values in the preview or clicking the ⬇️ Export button to edit the JSON in the playground editor at the bottom.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
  <p><label data-smark>Name:</label><input name="name" type="text" data-smark></p>
  <p><label data-smark>Age:</label><input name="age" type="number" data-smark></p>
  <p>
    <label data-smark>Favorite Color:</label>
    <span data-smark='{"type":"color","name":"color"}'>
      <input data-smark><button data-smark='{"action":"clear"}' title="Clear">βœ•</button>
    </span>
  </p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {"name":"Alice","age":28,"color":"#6366f1"}
});

πŸ‘‰ Null values. SmarkForm fields can be null to mean β€œunknown”. Unlike native HTML, even <input type="color"> can be null β€” just press Delete or use the βœ• trigger.

πŸ‘‰ Triggers. Elements with data-smark='{"action":"..."}' call actions on SmarkForm fields. Common actions: import, export, clear, addItem, removeItem.

πŸ‘‰ JSON editor. The Editor tab shows the form data as JSON. Edit and click outside to import your changes back.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Three-Level Nesting

This single HTML example packs a surprising range of SmarkForm features: three-level nesting of forms and lists, sortable groups, cross-list drag-and-drop for students, nested collapsible sections, auto-disabling triggers that respect min_items/max_items boundaries, per-subject grade lists that grow on demand, empty-list placeholders, and automatic position numbering β€” all from the declarative HTML you see in the source tab with no custom JavaScript. Each capability is introduced step by step in the pages that follow.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
  <div class="sg">
    <div class="sg-header">
      <input name="name" placeholder="School name" type="text" data-smark>
      <input name="level" placeholder="Level (e.g. Primary)" type="text" data-smark>
      <input name="year" placeholder="Academic year" type="text" data-smark>
    </div>

    <ul data-smark='{"type":"list","name":"groups","sortable":true,"min_items":0}'>
      <li data-smark='{"role":"empty_list"}' class="sg-empty">No groups yet</li>
      <li>
        <div class="sg-card">
          <details>
            <summary>
              <span data-smark='{"type":"label"}' class="sg-handle">β Ώ</span>
              <span data-smark='{"action":"position"}'>#</span>
              <input name="name" placeholder="Group name" type="text" data-smark>
              <button data-smark='{"action":"removeItem","hotkey":"-"}' title="Remove group">βž–</button>
            </summary>
            <div class="sg-body">
              <label class="sg-tutor">Tutor: <input name="tutor" placeholder="Tutor name" type="text" data-smark></label>
              <div class="sg-students">
                <strong>Students</strong>
                <ul data-smark='{"type":"list","name":"students","sortable":true,"movingDepth":2,"min_items":0}'>
                  <li data-smark='{"role":"empty_list"}' class="sg-empty">No students yet</li>
                  <li>
                    <details>
                      <summary>
                        <span data-smark='label' title="Drag to reorder or move between groups" class="sg-handle">β Ώ</span>
                        <input name="name" placeholder="Student name" type="text" data-smark>
                        <button data-smark='{"action":"removeItem","hotkey":"-"}' title="Remove student">βž–</button>
                      </summary>
                      <div data-smark='{"type":"form","name":"grades"}' class="sg-grades">
                        <div class="sg-grade-col">
                          <div class="sg-grade-label">Math</div>
                          <div data-smark='{"type":"list","name":"math","min_items":0}'>
                            <div data-smark='{"role":"empty_list"}' class="sg-empty">βˆ…</div>
                            <input type="number" step="0.1" min="0" max="10" data-smark>
                          </div>
                          <div class="sg-grade-btns">
                            <button data-smark='{"action":"addItem","hotkey":"+","context":"math"}' title="Add grade">βž•</button>
                            <button data-smark='{"action":"removeItem","hotkey":"-","context":"math"}' title="Remove grade">βž–</button>
                          </div>
                        </div>
                        <div class="sg-grade-col">
                          <div class="sg-grade-label">Literature</div>
                          <div data-smark='{"type":"list","name":"literature","min_items":0}'>
                            <div data-smark='{"role":"empty_list"}' class="sg-empty">βˆ…</div>
                            <input type="number" step="0.1" min="0" max="10" data-smark>
                          </div>
                          <div class="sg-grade-btns">
                            <button data-smark='{"action":"addItem","hotkey":"+","context":"literature"}' title="Add grade">βž•</button>
                            <button data-smark='{"action":"removeItem","hotkey":"-","context":"literature"}' title="Remove grade">βž–</button>
                          </div>
                        </div>
                        <div class="sg-grade-col">
                          <div class="sg-grade-label">Science</div>
                          <div data-smark='{"type":"list","name":"science","min_items":0}'>
                            <div data-smark='{"role":"empty_list"}' class="sg-empty">βˆ…</div>
                            <input type="number" step="0.1" min="0" max="10" data-smark>
                          </div>
                          <div class="sg-grade-btns">
                            <button data-smark='{"action":"addItem","hotkey":"+","context":"science"}' title="Add grade">βž•</button>
                            <button data-smark='{"action":"removeItem","hotkey":"-","context":"science"}' title="Remove grade">βž–</button>
                          </div>
                        </div>
                      </div>
                    </details>
                  </li>
                </ul>
                <button data-smark='{"action":"addItem","hotkey":"+","context":"students"}' title="Add student">βž• Add Student</button>
              </div>
            </div>
          </details>
        </div>
      </li>
    </ul>
    <button data-smark='{"action":"addItem","hotkey":"+","context":"groups"}' title="Add group">βž• Add Group</button>
  </div>
</div>
#myForm .sg { max-width: 520px; font-size: 0.95em; font-family: sans-serif; }
#myForm .sg ul { list-style: none; padding: 0; margin: 0; }
#myForm .sg-header {
    display: flex; gap: 0.5em; margin-bottom: 0.6em; flex-wrap: wrap;
}
#myForm .sg-header input {
    flex: 1; min-width: 120px; padding: 0.35em 0.5em;
    border: 1px solid #ccc; border-radius: 4px;
}
#myForm .sg-card {
    border: 1px solid #ddd; border-radius: 8px;
    margin: 0.4em 0; background: #fafafa;
    overflow: hidden;
}
#myForm .sg-card details[open] { padding-bottom: 0.4em; }
#myForm .sg-card summary {
    display: flex; align-items: center; gap: 0.4em;
    padding: 0.35em 0.5em; user-select: none;
    background: #fff; border-radius: 8px;
}
#myForm .sg-card details[open] summary { border-radius: 8px 8px 0 0; border-bottom: 1px solid #eee; }
#myForm .sg-handle { cursor: grab; color: #aaa; user-select: none; }
#myForm .sg-card summary input[type="text"] {
    flex: 1; padding: 0.25em 0.4em;
    border: 1px solid #ccc; border-radius: 4px;
}
#myForm .sg-body { padding: 0.4em 0.5em 0.2em 1.8em; }
#myForm .sg-tutor { display: flex; align-items: center; gap: 0.4em; margin-bottom: 0.3em; font-size: 0.9em; }
#myForm .sg-tutor input { flex: 1; padding: 0.2em 0.4em; border: 1px solid #ccc; border-radius: 4px; }
#myForm .sg-students { margin-top: 0.3em; }
#myForm .sg-students > strong { font-size: 0.85em; color: #555; display: block; margin-bottom: 0.2em; }
#myForm .sg-students ul {
    max-height: 240px; overflow-y: auto;
    border: 1px solid #eee; border-radius: 4px; padding: 0.2em 0.3em;
    background: #fff;
}
#myForm .sg-students ul li { margin: 0.15em 0; }
#myForm .sg-students ul details summary {
    display: flex; align-items: center; gap: 0.3em; padding: 0.2em 0.3em;
    border-radius: 4px; background: #fafafa; border: 1px solid #eee;
}
#myForm .sg-students ul details[open] summary { border-radius: 4px 4px 0 0; border-bottom: 0; }
#myForm .sg-students ul details[open] { background: #fafafa; }
#myForm .sg-students ul li input[type="text"] {
    flex: 1; padding: 0.2em 0.4em;
    border: 1px solid #ccc; border-radius: 4px; font-size: 0.9em;
}
#myForm .sg-grades {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
    padding: 0.3em 0.3em 0.2em 0.3em;
}
#myForm .sg-grade-col {
    display: flex; flex-direction: column; gap: 2px;
}
#myForm .sg-grade-label {
    font-weight: bold; text-align: center; font-size: 0.85em;
    padding: 0.15em 0; border-bottom: 1px solid #ddd;
}
#myForm .sg-grade-col input[type="number"] {
    width: 100%; box-sizing: border-box;
    padding: 0.15em 0.3em;
    border: 1px solid #ccc; border-radius: 4px;
}
#myForm .sg-grade-btns {
    display: flex; gap: 2px; justify-content: center; margin-top: 2px;
}
#myForm .sg-grade-btns button {
    padding: 0.1em 0.4em; border: 1px solid #ccc; border-radius: 4px;
    background: #fff; cursor: pointer; font-size: 0.85em; line-height: 1.4;
}
#myForm .sg-empty { font-style: italic; color: #aaa; padding: 0.5em; font-size: 0.9em; }
#myForm .sg button {
    padding: 0.2em 0.6em; border: 1px solid #ccc; border-radius: 4px;
    background: #fff; cursor: pointer; line-height: 1.5;
}
#myForm .sg button:disabled { opacity: 0.4; }
#myForm .sg [data-hotkey] { position: relative; }
#myForm .sg [data-hotkey]::after {
    content: "Ctrl+" attr(data-hotkey);
    position: absolute; top: -1.6em; left: 0;
    font-size: 0.7em; background: #333; color: #fff;
    padding: 1px 4px; border-radius: 3px; white-space: nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "name": "Springfield Elementary",
    "level": "Primary",
    "year": "2025/2026",
    "groups": [
        {
            "name": "Class A",
            "tutor": "Mr. Smith",
            "students": [
                {"name": "Lisa Simpson",  "grades": {"math": [9.5, 8.0], "literature": [7.3], "science": [8.8]}},
                {"name": "Bart Simpson",  "grades": {"math": [4.0], "literature": [5.5], "science": [3.0]}}
            ]
        },
        {
            "name": "Class B",
            "tutor": "Ms. Johnson",
            "students": [
                {"name": "Milhouse Van Houten", "grades": {"math": [6.0], "literature": [7.0], "science": [6.5]}}
            ]
        }
    ]
}
});

πŸ‘‰ Nested hierarchy. Three levels of nesting: groups β†’ students β†’ grades produce deeply structured JSON automatically.

πŸ‘‰ Card layout. Each group is a card with a scrollable student list (max-height, overflow-y: auto) β€” keeps the page tidy even with many students.

πŸ‘‰ Cross-list drag. movingDepth: 2 on the students list lets users drag students between groups (sibling distance = 2: student β†’ group β†’ sibling’s students).

πŸ‘‰ Collapsible sections. Both groups and students use <details>/<summary> to keep the view compact. A group shows its name in the summary; opening it reveals the tutor and student list. Likewise for each student.

πŸ‘‰ Nested <details>. SmarkForm handles keyboard navigation correctly even with nested collapsible sections β€” Shift+Space toggles, Enter navigates, and auto-open works as expected.

πŸ‘‰ Auto-disable. Triggers disable themselves at their list’s min_items/max_items boundary β€” no code needed.

πŸ‘‰ position action. Span with data-smark='{"action":"position"}' auto-numbers each group.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Deeply nested forms

Despite of usability concerns, there is no limit in form nesting depth.

In fact, all examples in this chapter are entirely built with SmarkForm itself with no additional JS code.

πŸš€ Including the JSON playground editor and the ⬇️ Export, ⬆️ Import, ♻️ Reset and ❌ Clear buttons are just SmarkForm trigger components that work out of the box.

πŸ€” …it’s just that part is omitted in the shown HTML source to keep the examples simple and focused on the subject they are intended to illustrate.

The editor scaffold (Export / Import / Reset / Clear buttons + the JSON textarea) is injected externally by the documentation framework. It is not part of the example HTML β€” so what you see in the HTML tab is exactly the code you would write yourself.

πŸ•΅οΈ If you go to any of the interactive examples in this page (or in the rest of the documentation) and check the πŸ“ Edit checkbox, you’ll be editing the real example source code. Check the πŸ“‹ Include playground editor checkbox to also show the editor scaffold in the preview (it is injected externally and is not part of the example HTML).

  • If you look close to the HTML source, you will see that ⬆️ Import and ⬇️ Export buttons import/export the whole form or individual fields from/to a textarea field called editor.

  • …And if you look at its JS tab you’ll see that in most of them there is no JavaScript code except for the SmarkForm instantiation itself.

πŸ‘‰ The whole SmarkForm form is a field of the type form that imports/exports JSON and πŸš€ they can be nested up to any depth.

  • The ⬇️ Export, ⬆️ Import and ❌ Clear buttons are trigger components that perform specialized actions (look at the HTML tab to see how…). πŸš€ No JavaScript wiring is needed.

In the Import and Export Data section we’ll go deeper into the import and export actions and how to get the most of them.

More on lists

SmarkForm’s lists are incredibly powerful and flexible. They can be used to create complex data structures, such as schedules, inventories, or any other repeating data structure.

To begin with, another interesting use case for lists is to create a schedule list like the following example:

The βž– and βž• buttons in the examples below use hotkeys. Press and hold the Ctrl key to see which ones are available. Check the CSS tab to see the reveal setup, or jump to Context-Driven Keyboard Shortcuts to learn more.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
    <p>
        <button data-smark='{"action":"removeItem","hotkey":"-","context":"schedule"}' title='Less intervals'>βž–</button>
        <button data-smark='{"action":"addItem","hotkey":"+","context":"schedule"}' title='More intrevals'>βž•</button>
        <strong data-smark="label">Schedule:</strong>
        <span data-smark='{"type":"list","name":"schedule","min_items":0,"max_items":3,"exportEmpties":true}'>
            <span>
                <input class='small' data-smark type='time' name='start'> to <input class='small' data-smark type='time' name='end'>
            </span>
            <span data-smark='{"role":"empty_list"}'>(Closed)</span>
            <span data-smark='{"role":"separator"}'>, </span>
            <span data-smark='{"role":"last_separator"}'> and </span>
        </span>
    </p>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "schedule": [
        {
            "start": "09:00:00",
            "end": "13:00:00"
        },
        {
            "start": "14:00:00",
            "end": "18:00:00"
        }
    ]
}
});

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

…This is fine for a simple case, and leaves the door open for easily increasing the number of intervals allowed in the schedule.

But it could look kind of messy if you need to introduce several schedules that may have different number of intervals.

Let’s imagine a hotel wanting to manage the scheduling of all the services it offers…

πŸ‘‰ We’ll see how this gets implemented in the Mixins section below, where the #scheduleRow template makes the pattern clean and reusable.

Mixins

The hotel scheduling example below uses a single mixin template (#scheduleRow) instead of repeating the same .schedule-row markup four times. With Mixin Types, you define that pattern once inside a <template> element and reference it from as many usage sites as you need.

Each usage site keeps its own identity:

  • name is supplied by the placeholder, not the template β€” every row gets its own field name and data path.
  • data-for slots β€” e.g. <span data-for="label"> replaces the <span id="label"> inside the template.
  • Option overrides β€” any data-smark in the placeholder overrides the template default (e.g. "max_items":5 on a specific row).

The <template> tag also accepts optional siblings:

  • <style> β€” injected into <head> exactly once, no matter how many times the mixin is used.
  • <script> β€” a per-instance hook (discussed later).

Press and hold Ctrl to reveal the available hotkeys on the corresponding βž– / βž• buttons depending on where the focus is.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
  <div class="schtbl" data-smark='{"type":"form","name":"schedules"}'>
    <div data-smark='{"type":"#scheduleRow","name":"rcpt_schedule"}'>
      <span data-for="label">πŸ›ŽοΈ Reception:</span></div>
    <div data-smark='{"type":"#scheduleRow","name":"bar_schedule"}'>
      <span data-for="label">🍸 Bar:</span></div>
    <div data-smark='{"type":"#scheduleRow","name":"restaurant_schedule"}'>
      <span data-for="label">🍽️ Restaurant:</span></div>
    <div data-smark='{"type":"#scheduleRow","name":"pool_schedule"}'>
      <span data-for="label">🏊 Pool:</span></div>
  </div>
</div>
<template id="scheduleRow">
  <div class="schedule-row"
       data-smark='{"type":"list","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
    <strong data-smark='{"role":"header"}'><span id="label">Schedule</span></strong>
    <span class='time_slot' data-smark='{"role":"empty_list"}'>(Closed)</span>
    <span class='time_slot'>
      <span class='time_from'>From <input class='small' data-smark type='time' name='start'></span>
      <span class='time_to'>to <input class='small' data-smark type='time' name='end'></span>
    </span>
    <span data-smark='{"role":"footer"}'>
      <button data-smark='{"action":"removeItem","hotkey":"-"}' title="Less intervals">βž–</button>
      <button data-smark='{"action":"addItem","hotkey":"+"}' title="More intervals">βž•</button>
    </span>
  </div>
  <style>
    .schtbl {
      display: flex; flex-direction: column; gap: 0.1em;
    }
    .schedule-row {
      display: grid;
      grid-template-columns: 10em 1fr auto;
      align-items: start;
      gap: 0.25em 0.5em;
      padding: 0.2em 0.4em;
      border-radius: 0.3em;
    }
    .schedule-row:nth-child(even) {
      background-color: rgba(128, 128, 128, 0.08);
    }
    .schedule-row > [data-role="header"] {
      grid-column: 1; grid-row: 1;
      padding-top: 0.3em;
    }
    .schedule-row > .time_slot    { grid-column: 2; }
    .schedule-row > [data-role="empty_list"] { padding-right: 5em; }
    .schedule-row > [data-role="footer"] {
      grid-column: 3; grid-row: 1 / -1; align-self: center; white-space: nowrap;
    }
    .time_slot {
      display: flex; flex-wrap: wrap; gap: .15em .4em; align-items: center;
      justify-content: flex-end;
    }
    .time_slot input.small { max-width: 5.5em; }
    .time_from, .time_to { display: flex; align-items: center; gap: .2em; white-space: nowrap; }
    @media (max-width: 30em) {
      .schedule-row { grid-template-columns: 1fr auto; }
      .schedule-row > .time_slot,
      .schedule-row > [data-role="empty_list"] {
        grid-column: 1; padding-left: 0.5em; text-align: right;
      }
      .schedule-row > [data-role="footer"] { grid-column: 2; grid-row: 2 / -1; }
    }
  </style>
</template>
#myForm .schtbl {
    display: flex;
    flex-direction: column;
    gap: 0.1em;
}
#myForm .schedule-row {
    display: grid;
    grid-template-columns: 10em 1fr auto;
    align-items: start;
    gap: 0.25em 0.5em;
    padding: 0.2em 0.4em;
    border-radius: 0.3em;
}
#myForm .schedule-row:nth-child(even) {
    background-color: rgba(128, 128, 128, 0.08);
}
#myForm .schedule-row > [data-role="header"] {
    grid-column: 1;
    grid-row: 1;
    padding-top: 0.3em;
}
#myForm .schedule-row > .time_slot {
    grid-column: 2;
}
#myForm .schedule-row > [data-role="empty_list"] {
    padding-right: 5em;
}
#myForm .schedule-row > [data-role="footer"] {
    grid-column: 3;
    grid-row: 1 / -1;
    align-self: center;
    white-space: nowrap;
}
#myForm .time_slot {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15em 0.4em;
    align-items: center;
    justify-content: flex-end;
}
#myForm .time_slot input.small{
    max-width: 5.5em;
}
#myForm .time_from,
#myForm .time_to {
    display: flex;
    align-items: center;
    gap: 0.2em;
    white-space: nowrap;
}
#myForm .period-dates {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25em 1.5em;
    align-items: baseline;
    margin: 0.3em 0;
    justify-content: flex-end;
}
#myForm .period-date {
    white-space: nowrap;
}
@media (max-width: 30em) {
  #myForm .schedule-row {
      grid-template-columns: 1fr auto;
  }
  #myForm .schedule-row > [data-role="header"] {
      grid-column: 1;
      grid-row: 1;
      padding-top: 0;
  }
  #myForm .schedule-row > .time_slot,
  #myForm .schedule-row > [data-role="empty_list"] {
      grid-column: 1;
      padding-left: 0.5em;
      text-align: right;
  }
  #myForm .schedule-row > [data-role="footer"] {
      grid-column: 2;
      grid-row: 2 / -1;
  }
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "schedules": {
        "rcpt_schedule": [
            {
                "start": "00:00:00",
                "end": "23:59:00"
            }
        ],
        "bar_schedule": [
            {
                "start": "11:00:00",
                "end": "23:00:00"
            }
        ],
        "restaurant_schedule": [
            {
                "start": "07:30:00",
                "end": "10:30:00"
            },
            {
                "start": "13:00:00",
                "end": "15:30:00"
            },
            {
                "start": "19:00:00",
                "end": "22:00:00"
            }
        ],
        "pool_schedule": [
            {
                "start": "09:00:00",
                "end": "20:00:00"
            }
        ]
    }
}
});

πŸ‘‰ Here we replaced the original <table> layout with CSS grid to prevent horizontal scrollbars when multiple intervals are added:

  • Each schedule list (.schedule-row) is a CSS grid with three columns: 10em label | 1fr slots | auto controls.
  • Additional intervals stack vertically in the middle column instead of widening the row.
  • The footer role holds the βž–/βž• buttons, which span all slot rows via grid-row: 1 / -1 so they stay right-aligned regardless of item count.

πŸ‘‰ The header, footer and empty_list template roles are still used, but the placeholder had been removed since the grid handles column sizing without needing DOM filler elements.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Want to learn more about mixins? See the full reference in Mixin Types.

Nested lists and forms

Great! Now we have all the scheduling information of or hotel services.

…or maybe not:

Some services may have different schedules for different days of the week or depending on the season (think in the swimming pool in winter…).

Since we can make lists of forms, we can also nest more forms and lists inside every list item and so forth to any depth.

πŸ‘‰ Let’s focus on the seasons by now:

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
  <h2>πŸ—“οΈ Periods:</h2>
  <div data-smark='{"type":"list","name":"periods","sortable":true,"exportEmpties":true,"min_items":0,"value":[{}]}'>
    <fieldset data-smark='{"role":"empty_list"}' style='text-align: center'>πŸ”’ Out of Service</fieldset>
    <div data-smark='{"type":"#periodItem"}'></div>
  </div>
  <button
    data-smark='{"action":"addItem","context":"periods","hotkey":"+"}'
    style="float: right; margin-top: 1em"
  >βž• Add Period</button>
</div>
<template id="periodItem">
  <fieldset data-smark='{"type":"form","exportEmpties":true}' style='margin-top: 1em'>
    <legend>Period
      <span data-smark='{"action":"position"}'>N</span>
      of
      <span data-smark='{"action":"count"}'>M</span>
    </legend>
    <button
      data-smark='{"action":"addItem","source":".-1","hotkey":"d"}'
      title='Duplicate this period'
      style="float: right"
    >✨</button>
    <button
      data-smark='{"action":"removeItem","hotkey":"-"}'
      title='Remove this period'
      style="float: right"
    >βž–</button>
    <p class='period-dates'>
      <span class='period-date'><label data-smark>Start Date:</label>&nbsp;<input data-smark type='date' name='start_date'></span>
      <span class='period-date'><label data-smark>End Date:</label>&nbsp;<input data-smark type='date' name='end_date'></span>
    </p>
    <div class="schtbl" data-smark='{"type":"form","name":"schedules"}'>
      <div data-smark='{"type":"#scheduleRow","name":"rcpt_schedule"}'>
        <span data-for="label">πŸ›ŽοΈ Reception:</span></div>
      <div data-smark='{"type":"#scheduleRow","name":"bar_schedule"}'>
        <span data-for="label">🍸 Bar:</span></div>
      <div data-smark='{"type":"#scheduleRow","name":"restaurant_schedule"}'>
        <span data-for="label">🍽️ Restaurant:</span></div>
      <div data-smark='{"type":"#scheduleRow","name":"pool_schedule"}'>
        <span data-for="label">🏊 Pool:</span></div>
    </div>
  </fieldset>
  <script>
    const item = this;
    item.parent.on('AfterAction_addItem', async function(ev) {
      if (!ev.data || ev.data.getPath() !== item.getPath()) return;
      const idx   = parseInt(item.getPath().split('/').pop());
      const items = await item.parent.export({ exportEmpties: true });
      const prev  = idx > 0               ? items[idx - 1] : null;
      const next  = idx < items.length - 1 ? items[idx + 1] : null;
      const fmtDate = d =>
        d.getFullYear() + '-' +
        String(d.getMonth() + 1).padStart(2, '0') + '-' +
        String(d.getDate()).padStart(2, '0');
      let startDate = null;
      if (prev?.end_date) {
        const d = new Date(prev.end_date + 'T00:00:00');
        d.setDate(d.getDate() + 1);
        startDate = fmtDate(d);
      } else if (!prev) {
        startDate = fmtDate(new Date());
      }
      let endDate = null;
      if (startDate && !next) {
        if (prev?.start_date && prev?.end_date) {
          const pStart = new Date(prev.start_date + 'T00:00:00');
          const pEnd   = new Date(prev.end_date   + 'T00:00:00');
          const nStart = new Date(startDate + 'T00:00:00');
          const pStartsFirst = pStart.getDate() === 1;
          const pEndsLast = pEnd.getDate() ===
            new Date(pEnd.getFullYear(), pEnd.getMonth() + 1, 0).getDate();
          if (pStartsFirst && pEndsLast) {
            const months = (pEnd.getFullYear() - pStart.getFullYear()) * 12 +
              (pEnd.getMonth() - pStart.getMonth()) + 1;
            endDate = fmtDate(
              new Date(nStart.getFullYear(), nStart.getMonth() + months, 0));
          } else {
            const days = Math.round((pEnd - pStart) / 86400000);
            const d = new Date(nStart);
            d.setDate(d.getDate() + days);
            endDate = fmtDate(d);
          }
        }
      } else if (startDate && next?.start_date) {
        if (next.start_date > startDate) {
          const d = new Date(next.start_date + 'T00:00:00');
          d.setDate(d.getDate() - 1);
          endDate = fmtDate(d);
        }
      }
      if (endDate && startDate && endDate < startDate) endDate = null;
      const startField = item.find('start_date');
      const endField   = item.find('end_date');
      if (startField) await startField.import(startDate, { setDefault: false });
      if (endField)   await endField.import(endDate,   { setDefault: false });
    });
  </script>
</template>
<template id="scheduleRow">
  <div class="schedule-row"
       data-smark='{"type":"list","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
    <strong data-smark='{"role":"header"}'><span id="label">Schedule</span></strong>
    <span class='time_slot' data-smark='{"role":"empty_list"}'>(Closed)</span>
    <span class='time_slot'>
      <span class='time_from'>From <input class='small' data-smark type='time' name='start'></span>
      <span class='time_to'>to <input class='small' data-smark type='time' name='end'></span>
    </span>
    <span data-smark='{"role":"footer"}'>
      <button data-smark='{"action":"removeItem","hotkey":"-"}' title="Less intervals">βž–</button>
      <button data-smark='{"action":"addItem","hotkey":"+"}' title="More intervals">βž•</button>
    </span>
  </div>
  <style>
    .schtbl {
      display: flex; flex-direction: column; gap: 0.1em;
    }
    .schedule-row {
      display: grid;
      grid-template-columns: 10em 1fr auto;
      align-items: start;
      gap: 0.25em 0.5em;
      padding: 0.2em 0.4em;
      border-radius: 0.3em;
    }
    .schedule-row:nth-child(even) {
      background-color: rgba(128, 128, 128, 0.08);
    }
    .schedule-row > [data-role="header"] {
      grid-column: 1; grid-row: 1;
      padding-top: 0.3em;
    }
    .schedule-row > .time_slot    { grid-column: 2; }
    .schedule-row > [data-role="empty_list"] { padding-right: 5em; }
    .schedule-row > [data-role="footer"] {
      grid-column: 3; grid-row: 1 / -1; align-self: center; white-space: nowrap;
    }
    .time_slot {
      display: flex; flex-wrap: wrap; gap: .15em .4em; align-items: center;
      justify-content: flex-end;
    }
    .time_slot input.small { max-width: 5.5em; }
    .time_from, .time_to { display: flex; align-items: center; gap: .2em; white-space: nowrap; }
    @media (max-width: 30em) {
      .schedule-row { grid-template-columns: 1fr auto; }
      .schedule-row > .time_slot,
      .schedule-row > [data-role="empty_list"] {
        grid-column: 1; padding-left: 0.5em; text-align: right;
      }
      .schedule-row > [data-role="footer"] { grid-column: 2; grid-row: 2 / -1; }
    }
  </style>
</template>
#myForm .schtbl {
    display: flex;
    flex-direction: column;
    gap: 0.1em;
}
#myForm .schedule-row {
    display: grid;
    grid-template-columns: 10em 1fr auto;
    align-items: start;
    gap: 0.25em 0.5em;
    padding: 0.2em 0.4em;
    border-radius: 0.3em;
}
#myForm .schedule-row:nth-child(even) {
    background-color: rgba(128, 128, 128, 0.08);
}
#myForm .schedule-row > [data-role="header"] {
    grid-column: 1;
    grid-row: 1;
    padding-top: 0.3em;
}
#myForm .schedule-row > .time_slot {
    grid-column: 2;
}
#myForm .schedule-row > [data-role="empty_list"] {
    padding-right: 5em;
}
#myForm .schedule-row > [data-role="footer"] {
    grid-column: 3;
    grid-row: 1 / -1;
    align-self: center;
    white-space: nowrap;
}
#myForm .time_slot {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15em 0.4em;
    align-items: center;
    justify-content: flex-end;
}
#myForm .time_slot input.small{
    max-width: 5.5em;
}
#myForm .time_from,
#myForm .time_to {
    display: flex;
    align-items: center;
    gap: 0.2em;
    white-space: nowrap;
}
#myForm .period-dates {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25em 1.5em;
    align-items: baseline;
    margin: 0.3em 0;
    justify-content: flex-end;
}
#myForm .period-date {
    white-space: nowrap;
}
@media (max-width: 30em) {
  #myForm .schedule-row {
      grid-template-columns: 1fr auto;
  }
  #myForm .schedule-row > [data-role="header"] {
      grid-column: 1;
      grid-row: 1;
      padding-top: 0;
  }
  #myForm .schedule-row > .time_slot,
  #myForm .schedule-row > [data-role="empty_list"] {
      grid-column: 1;
      padding-left: 0.5em;
      text-align: right;
  }
  #myForm .schedule-row > [data-role="footer"] {
      grid-column: 2;
      grid-row: 2 / -1;
  }
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {"allowLocalMixinScripts":"allow",
    "value": {
    "periods": [
        {
            "start_date": "2025-01-01",
            "end_date": "2025-06-30",
            "schedules": {
                "rcpt_schedule": [
                    {"start": "00:00:00", "end": "23:59:00"}
                ],
                "bar_schedule": [
                    {"start": "10:00:00", "end": "23:30:00"}
                ],
                "restaurant_schedule": [
                    {"start": "07:30:00", "end": "10:30:00"},
                    {"start": "13:00:00", "end": "15:30:00"},
                    {"start": "19:00:00", "end": "22:00:00"}
                ],
                "pool_schedule": []
            }
        },
        {
            "start_date": "2025-07-01",
            "end_date": "2025-12-31",
            "schedules": {
                "rcpt_schedule": [
                    {"start": "00:00:00", "end": "23:59:00"}
                ],
                "bar_schedule": [
                    {"start": "10:00:00", "end": "23:30:00"}
                ],
                "restaurant_schedule": [
                    {"start": "07:30:00", "end": "10:30:00"},
                    {"start": "13:00:00", "end": "15:30:00"},
                    {"start": "19:00:00", "end": "22:00:00"}
                ],
                "pool_schedule": [
                    {"start": "09:30:00", "end": "19:30:00"}
                ]
            }
        }
    ]
}
});

πŸ‘‰ Two templates, one example:

  • #scheduleRow (inner) β€” the time-interval list template.
  • #periodItem (outer) β€” wraps the whole period fieldset and uses #scheduleRow inside it, demonstrating mixin composition.

πŸ‘‰ Smart date prefill (the <script> in #periodItem):

  • Registers an AfterAction_addItem listener that fires after the source:".-1" import, so it always operates on the final data.
  • Computes start_date and end_date based on siblings:
Situation start_date end_date
No previous period today blank
Has previous (no end_date) blank blank
Has previous, no next prev.end + 1 day start + same duration as prev
Inserted β€” gap exists prev.end + 1 day next.start βˆ’ 1 day
Inserted β€” contiguous prev.end + 1 day blank (user decides)
  • end_date is cleared whenever it would predate start_date.

πŸ‘‰ Empty-list message. Remove all periods to see πŸ”’ Out of Service.

πŸ‘‰ Sortable periods. Drag and drop to reorder.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

⚑ There is no theoretical limit to the depth of nesting beyond the logical usability concerns.

<script> hooks inside mixin templates are a powerful way to add component-specific behavior.

Within them, this refers to the individual mixin instance, allowing you to listen to events and manipulate data at the item level, without affecting other instances of the same mixin.

The former example uses this feature to implement smart date prefill logic that considers the position of the new period and its siblings to suggest appropriate start and end dates, streamlining the user experience when adding new periods.

πŸš€ Let’s try it!

Item duplication and closure state

Adding similar items to a list of complex and configurable subforms β€”like the periods list in our exampleβ€” can be tedious if users have to re-enter all fields each time.

The previous example already includes a duplicate (✨) button that uses source:".-1" to prefill a new item with data copied from the previous one. It also sets min_items:0 with value:[{}] so the list starts with one empty item (good UX) yet can be emptied entirely, at which point the πŸ”’ Out of Service message (the empty_list role) is shown.

These techniques work together:

  • source:".-1" on an addItem trigger β€” the new item receives the previous item’s data immediately after render.
  • min_items:0 β€” allows the list to be fully emptied.
  • value:[{}] β€” ensures one empty item appears by default even with min_items:0, so users are never greeted with a blank list.
  • empty_list role β€” provides feedback when the list has no items.

A note on empty values

Take a look to the HTML source of the previous example and pay attention to where and how the exportEntries property is used in the lists:

  • For the periods list we set exportEmpties to true, overidding its default value (false).
    • This way, if a period is added (intentional), it gets exported even if not filled.
    • This is because the user may be saving his work to continue later or just mean there is a period but we don’t know its data yet.
  • For the schedules lists we set exportEmpties to false (necessary to prevent inheriting the true value we just set). This way:
    • When a period is added, all schedules are layed out with their default value (one empty time interval ready to be filled).
    • If the user leaves any unfilled (because of being inappropriate) and neglects removing it, it will be just swallowed when exporting the form data.
    • This way, when importing the exported data (or if item is duplicated with the ✨ button), the unfilled intervals are correctly shown as β€œ(Closed)”.

Nesting Mixins

The nested periods example already combines two mixins: #periodItem (the outer one, wrapping the period fieldset) references #scheduleRow (the inner one, providing the time-interval grid). The <style> from #scheduleRow is injected once into <head> regardless of how many times either mixin is used β€” mixin styles are shared, not duplicated.

Looking at that example’s HTML source you can see both templates defined side by side: #scheduleRow is the inner list template; #periodItem is the outer component that uses it. Extract any repeated piece of UI into a similar template and reference it with type: "#yourTemplateName".

Import and Export Data

Exporting and importing data in SmarkForm cannot be easier.

The ⬇️ Export, ⬆️ Import and ❌ Clear buttons used in all examples in this documentation are just triggers that call the export and import actions on the whole form (their context):

  • ⬇️ Export exports the whole form to the β€œeditor” textarea (its target).
  • ⬆️ Import imports the JSON data from the β€œeditor” textarea into the form (its target).
  • ❌ Clear clears the whole form (its context).

The editor scaffold (Export/Import/Reset/Clear buttons + textarea) is injected externally by the documentation framework β€” it is not part of the example HTML source. You can see this by checking πŸ“ Edit on any example; the source tabs show only the real example code.

Intercepting the import and export events

Below these lines you can see the exact same form with additional πŸ’Ύ Save and πŸ“‚ Load buttons.

They are export and import triggers, but placed outside of any subform so that their natural context is the whole form.

In the JS tab there is a simple JavaScript code that:

  • Intercepts the onAfterAction_export and onBeforeAction_import events.
  • Shows the JSON of the whole form in a window.alert(...) window in the case of export (πŸ’Ύ) action.
  • Prompts with a window.prompt(...) dialog for JSON data to import into the whole form.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<form id="myForm"
  action="mailto:you@example.com?subject=Contact%20Form%20Submission"
  method="post"
  enctype="text/plain"
>
  <p>
    <label data-smark>Abstract</label>
    <input data-smark type="text"
      name="name"
      placeholder="Brief summary or description"
    />
  </p>
  <p>
    <label data-smark>Reason for contacting us:</label>
    <select data-smark name="reason" required>
      <option value="" disabled selected>β€” Choose β€”</option>
      <option value="question">Question</option>
      <option value="support">Support / Technical help</option>
      <option value="feedback">Suggestion or feedback</option>
      <option value="complaint">Complaint</option>
      <option value="praise">Praise / Thank you</option>
      <option value="business">Business / Sales inquiry</option>
      <option value="other">Something else</option>
    </select>
  </p>
  <p>
    <label data-smark>Message:</label>
    <textarea data-smark name="message"></textarea>
  </p>
  <p>
    <button data-smark='{"action":"submit"}'>πŸ“§ Send Email</button>
  </p>
</form>
const myForm = new SmarkForm(document.getElementById("myForm"));

πŸ‘‰ Clicking πŸ“§ Send Email opens the user’s email client with:

  • To: test@example.com
  • Subject: Contact Form Submission
  • Body: the Text-encoded form fields.

✏️ To use a real address:

  • Head to the πŸ—’οΈ HTML tab and check the πŸ“ checkbox.
  • Edit the email in the action attribute of the <form> element.
  • Click the ▢️ Run button to reload the πŸ‘οΈ Preview tab with the updated code.
  • Fill the form and click the πŸ“§ Send Email button.

🌐 To submit to an HTTP endpoint instead, point action at your server URL and propperly adjust the method attribute.

πŸ“¦ For JSON APIs, additionally set enctype="application/json" β€” SmarkForm will send the data as a JSON payload via fetch().

enctype="application/json" is not compatible with mailto: actions. Use the default (URL-encoded) encoding for mailto:.

You can also intercept or extend the submission via SmarkForm events: BeforeAction_submit (fired before sending β€” you can preventDefault() to cancel) and AfterAction_submit (fired after the data has been sent).

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

A note on context of the triggers

As we have seen in the previous examples:

  • We can use the export and import actions to export/import data from/to any context: The whole form, any of its subforms or even a single field.

  • That context is, by default, determined by the place where the trigger is placed in the DOM tree, but it can be explicitly set by the context property of the trigger component.

  • We can use the target property to set the destination/source of that data or intercept the afterAction_export and beforeAction_import events to programatically handle the data.

For the sake of simplicity, from now on, we’ll stick to the layout of the very first example (⬇️ Export, ⬆️ Import and ❌ Clear buttons targetting the β€œeditor” textarea) that doesn’t need any additional JS code.

That part of the layout will also be omitted in the HTML source since we’ve already know how it works.

πŸ‘Œ If you want a clearer example on how the context affect the triggers, take a look to the following example:

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
    <div data-smark='{"name":"demo"}'>
        <p>
            <label data-smark>Name:</label>
            <input name='name' data-smark>
        </p>
        <p>
            <label data-smark>Surname:</label>
            <input name='surname' data-smark>
        </p>
        <table>
            <tr style="text-align:center">
                <th>Name field:</th>
                <th>Surname field:</th>
                <th>Whole Form:</th>
            </tr>
            <tr style="text-align:center">
                <td><button data-smark='{"action":"import","context":"name","target":"/editor"}'>⬆️  Import</button></td>
                <td><button data-smark='{"action":"import","context":"surname","target":"/editor"}'>⬆️  Import</button></td>
                <td><button data-smark='{"action":"import","target":"/editor"}'>⬆️  Import</button></td>
            </tr>
            <tr style="text-align:center">
                <td><button data-smark='{"action":"export","context":"name","target":"/editor"}'>⬇️  Export</button></td>
                <td><button data-smark='{"action":"export","context":"surname","target":"/editor"}'>⬇️  Export</button></td>
                <td><button data-smark='{"action":"export","target":"/editor"}'>⬇️  Export</button></td>
            </tr>
            <tr style="text-align:center">
                <td><button data-smark='{"action":"clear","context":"name"}'>❌ Clear</button></td>
                <td><button data-smark='{"action":"clear","context":"surname"}'>❌ Clear</button></td>
                <td><button data-smark='{"action":"clear"}'>❌ Clear</button></td>
            </tr>
        </table>
    </div>
    <div style="display: flex; flex-direction:column; align-items:left; gap: 1em; width: 100%">
        <textarea
            cols="20"
            placeholder="JSON playground editor"
            data-smark='{"name":"editor","type":"input"}'
            style="resize: vertical; align-self: stretch; min-height: 8em; flex-grow: 1;"
        ></textarea>
    </div>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));

πŸ‘‰ Notice that all Import and Export buttons (triggers) are handled by the same event handlers (for β€œBeforeAction_import” and β€œAfterAction_export”, respectively).

πŸ‘‰ All Import and Export buttons (triggers) belong to different SmarkForm fields determined by (1) where they are placed in the DOM and (2) the relative path from that place pointed by the context property.

ℹ️ Different field types may import/export different data types (forms import/export JSON while regular inputs import/export text –or number–).

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

πŸš€ As you can see, the same actions can be applied to different parts of the form just by placing the triggers in the right place or explicitly setting the right path to the desired context.

πŸ‘‰ You can import, export or clear either the whole form or any of its fields. Try exporting / exporting / clearing the whole form or individual fields with the help of the β€œJSON data viewer / editor”.

Advanced UX Improvements

Finally, we’ll showcase some advanced user experience improvements that SmarkForm offers, such as smart auto-enabling/disabling of controls and non-breaking unobtrusive keyboard navigation among others.

Auto enabling or disabling of actions

As you may have already noticed, SmarkForm automatically enables or disables actions based on the current state of the form. For example, if a list has reached its maximum number of items specified by the max_items option, the β€œAdd Item” button will be disabled until an item is removed.

The same happen with the β€œRemove Item” button when the list has reached its minimum number of items specified by min_items.

Let’s recall our Singleton List Example with just slight modifications:

  1. Keep the min_items to its default value of 1, so that the list cannot be empty.
  2. Add a little CSS to make the disabled buttons more evident.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
  <button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "preserve_non_empty":true}' title='Remove unused fields'>🧹</button>
  <button data-smark='{"action":"removeItem", "context":"phones", "preserve_non_empty":true}' title='Remove phone number'>βž–</button>
  <button data-smark='{"action":"addItem","context":"phones"}' title='Add phone number'>βž• </button>
  <strong data-smark="label">Phones:</strong>
  <ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
    <li class="row">
      <label data-smark>πŸ“ž Telephone
      <span data-smark='{"action":"position"}'>N</span>
      </label>
      <button data-smark='{"action":"removeItem"}' title='Remove this phone number'>βž–</button>
      <input type="tel" data-smark>
      <button data-smark='{"action":"addItem"}' title='Insert phone number'>βž• </button>
    </li>
  </ul>
</div>
/* Hide list bullets */
#myForm ul li {
    list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
    opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "phones": [
        "+1 555 867 5309",
        "+1 555 234 5678"
    ]
}
});

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

πŸ‘‰ Notice that the 🧹 and βž– buttons get disabled then the list has only one item (at the beginning or after removing enough items to reach min_items’ value) and the same happens with the βž• button when the list reaches its max_items limit.

Context-Driven Keyboard Shortcuts (Hot Keys)

All SmarkForm triggers can be assigned a hotkey property to make them accessible via keyboard shortcuts.

To trigger an action using a keyboard shortcut the user only needs to press the Ctrl key and the key defined in the hotkey property of the trigger.

In the following example you can use the Ctrl++ and Ctrl+- combinations to add or remove phone numbers from the list, respectively.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
    <button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>🧹</button>
    <button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>βž–</button>
    <button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>βž• </button>
    <strong data-smark="label">Phones:</strong>
    <ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
        <li class="row">
            <label data-smark>πŸ“ž Telephone
            <span data-smark='{"action":"position"}'>N</span>
            </label>
            <button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>βž–</button>
            <input type="tel" data-smark>
            <button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>βž• </button>
        </li>
    </ul>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
    list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
    opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "phones": [
        "+1 555 867 5309",
        "+1 555 234 5678"
    ]
}
});

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Reveal of hot keys

If you tinkered a bit with the previous example, you may have noticed that as soon as you press the Ctrl key, the related hot keys are revealed beside corresponding buttons.

πŸš€ This means that the user does not need to know every hotkeys in advance, but can discover them on the fly by pressing the Ctrl key.

For instance I bet you already discovered that you can use the Ctrl+Delete combination to activate the 🧹 button and remove all unused phone number fields in the list.

For this to work, a little CSS setup is needed to define how the hint will look like.

Hotkey hints are dynamically revealed/unrevealied by setting/removing the data-hotkey attribute in the trigger’s DOM node.

Check the CSS tab of the example above to see an example of how to style the hot keys hints.

Hotkeys and context

In SmarkForm, hotkeys are context-aware, meaning that the same hotkey can trigger different actions depending on the context in which the focus is.

If you dug a bit into the HTML source of the previous example, you may have noticed that the outer βž• and βž– buttons have the hotkey property set as well but, unlike the 🧹 button, they are not announced when pressing the Ctrl key.

The reason behind this is that the value of their hotkey property is the same of their inner counterparts and hotkeys are discovered from the inner focused field to the outside, giving preference to the innermost ones in case of conflict.

Let’s see the same example with a few additional fields outside the list:

If you focus one of them and press the Ctrl key, you’ll see that nothing happens. But if you navigate to any phone number in the list (for instance by repeatedly pressing the Tab key) and press the Ctrl key, you’ll see that now the hotkeys we defined are available again.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
    <p>
        <label data-smark='{"type": "label"}'>Name:</label>
        <input name='name' data-smark='{"type": "input"}' />
    </p>
    <p>
        <label data-smark='{"type": "label"}'>Surname:</label>
        <input name='surname' data-smark='{"type": "input"}' />
    </p>
    <button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>🧹</button>
    <button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>βž–</button>
    <button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>βž• </button>
    <strong data-smark="label">Phones:</strong>
    <ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
        <li class="row">
            <label data-smark>πŸ“ž Telephone
            <span data-smark='{"action":"position"}'>N</span>
            </label>
            <button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>βž–</button>
            <input type="tel" data-smark>
            <button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>βž• </button>
        </li>
    </ul>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
    list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
    opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "name": "John",
    "surname": "Doe",
    "phones": [
        "+1 555 867 5309",
        "+1 555 234 5678"
    ]
}
});

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Collapsible sections

HTML’s native <details> and <summary> elements provide a simple, accessible way to create collapsible content β€” no JavaScript or special SmarkForm properties needed. SmarkForm fields placed inside a <details> element work exactly as they would anywhere else; the browser handles the show/hide toggle natively.

A particularly useful pattern is to use <details> elements as list items, placing an identifying field inside the <summary>. This way, even when an item is collapsed, its key information remains visible so the user can quickly scan the list.

The following example shows a contact list where each item is a <details> element. The <summary> contains the contact’s name so it is always visible, while the full details (email and phone) are revealed on expansion:

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
  <div data-smark='{"name":"contacts","type":"list","min_items":1,"exportEmpties":false}'>
    <details>
      <summary>
        <input data-smark type="text" name="fullname" placeholder="Full name">
      </summary>
      <div class="contact-details">
        <input data-smark type="email" name="email" placeholder="Email">
        <input data-smark type="tel" name="phone" placeholder="Phone">
        <button data-smark='{"action":"removeItem","failback":"clear","hotkey":"Delete"}' title="Remove">πŸ—‘</button>
      </div>
    </details>
  </div>
  <button data-smark='{"action":"addItem","context":"contacts","hotkey":"+"}'>βž• Add contact</button>
  <textarea data-smark name="notes" placeholder="Notes about this contact list"></textarea>
</div>
#myForm {
  font-family: sans-serif;
}
#myForm details {
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-bottom: 6px;
  padding: 4px 8px;
}
#myForm summary {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
#myForm summary::before {
  content: "β–Ά";
  font-size: .75em;
  transition: transform .15s;
  flex-shrink: 0;
}
#myForm details[open] > summary::before {
  transform: rotate(90deg);
}
#myForm summary input {
  flex: 1;
  min-width: 0;
  font-weight: bold;
  border: none;
  background: transparent;
  outline: none;
}
#myForm .contact-details {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 0 2px 1.2em;
}
#myForm .contact-details input {
  flex: 1;
  min-width: 120px;
}
#myForm .contact-details button {
  margin-left: auto;
}
#myForm textarea[name="notes"] {
  display: block;
  width: 100%;
  min-height: 60px;
  margin-top: 8px;
  padding: 6px 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-family: inherit;
  box-sizing: border-box;
  resize: vertical;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
  "contacts": [
    {"fullname":"Alice Smith","email":"alice@example.com","phone":"+1 555 100 0001"},
    {"fullname":"Bob Jones","email":"bob@example.com","phone":"+1 555 100 0002"},
    {"fullname":"Carol White","email":"carol@example.com","phone":"+1 555 100 0003"}
  ],
  "notes": ""
}
});

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

πŸ‘‰ Notice that clicking a contact’s name row (or the arrow at the left) toggles it. The name input inside <summary> is always visible and editable regardless of the collapsed/expanded state.

The same technique works for any form section, not just list items. Wrap a <div data-smark> or <fieldset data-smark> in a <details> element and add a <summary> heading β€” no SmarkForm-specific options needed. The open attribute on <details> controls the initial state: include it to start expanded (the default), or omit it to start collapsed.

Smooth navigation

As you may have already noticed in the preceding examples, SmarkForm provides an intuitive interface to facilitate users effortlessly discover how to fluently fill all the data in the form without bothering with the interface.

πŸ‘‰ Notice you can navigate smoothly between form fields by typing Enter (forward) and Shift+Enter (backward).

So, when you finish filling a field, you can just press Enter to move to the next one.

This is not only more convenient than Tab and Shift+Tab. More than that: it skips controls providing a more fluid experience when you are just filling data in.

In case of a textarea, use Ctrl+Enter instead, since Enter alone is used to insert a new line in the text.

Take a look to the πŸ“ Notes tab of the previous example for more interesting insights and tips.

πŸ‘‰ Last but not least, if you still prefer using Tab and Shift+Tab, in the previous example you may have noticed that you can navigate through the outer 🧹, βž• and βž– buttons using the Tab key, but you cannot navigate to the inner βž– and βž• buttons in every list item.

This is automatically handled by SmarkForm to improve User Experience:

  • Passing through all βž– and βž• buttons in every list item would have made it hard to navigate through the list.

  • SmarkForm detects that they have a hotkey defined and take them out of the navigation flow since the user only needs to press the Ctrl key to discover a handy alternative to activate them from the keyboard.

  • The outer ones, by contrast, are always kept in the navigation flow since they are outside of their actual context and their functionality may be required before having chance to bring the focus inside their context.

    • Put in other words: otherwise, with min_items set to 0, it would be impossible to create the first item without resorting to the mouse.

2nd level hotkeys

Let’s recall the previous example with few personal data and a list of phones and wrap it in a list to build a simple phonebook.

As we’ve learned, we can use β€œ+” and β€œ-β€œ hotkeys to add or remove entries in our phonebook without causing any conflict. When the user presses the Ctrl key the proper hotkeys are revealed depending on the context of the current focus.

πŸ€” But now let’s say you filled in the last phone number in the current entry and you want to add a new contact to the phonebook without turning to the mouse. You cannot reach the outer βž• button to add a new contact because its hotkey is the same as the inner βž• button to add a new phone number.

πŸš€ For this kind of situations, SmarkForm provides a 2nd level hotkey access:

πŸ‘‰ Just combine the Alt key with the Ctrl key and the hotkeys in their nearest level will be automatically inhibited allowing those in the next higher level to reveal.

Try it in the following example:

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
    <div data-smark='{"type": "list", "name": "phonelist", "sortable": true}'>
        <fieldset>
            <legend>
                <span data-smark='{"action":"removeItem", "hotkey":"-"}' title='Delete this phonebook entry' style='cursor:pointer'>[βž–]</span>
                <strong>
                    Contact
                    <span data-smark='{"action":"position"}'>N</span>
                </strong>
            </legend>            <p>
                <label data-smark='{"type": "label"}'>Name:</label>
                <input name='name' data-smark='{"type": "input"}' />
            </p>
            <p>
                <label data-smark='{"type": "label"}'>Surname:</label>
                <input name='surname' data-smark='{"type": "input"}' />
            </p>
            <button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>🧹</button>
            <button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>βž–</button>
            <button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>βž• </button>
            <strong data-smark="label">Phones:</strong>
            <ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
                <li class="row">
                    <label data-smark>πŸ“ž Telephone
                    <span data-smark='{"action":"position"}'>N</span>
                    </label>
                    <button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>βž–</button>
                    <input type="tel" data-smark>
                    <button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>βž• </button>
                </li>
            </ul>        </fieldset>
    </div>
    <p style="text-align: right; margin-top: 1em">
        <b>Total entries:</b>
        <span data-smark='{"action":"count", "context": "phonelist"}'>M</span>
    </p>
    <button
        data-smark='{"action":"addItem","context":"phonelist","hotkey":"+"}'
        style="float: right; margin-top: 1em"
    >βž• Add Contact</button>

</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
    list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
    opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"));

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Hidden actions

As we already learned, SmarkForm hotkeys are defined over trigger components so, to define a hotkey to perform some action, we need to place a trigger component that calls that action somewhere in the form.

This aligns well with the SmarkForm philosophy of providing a consistent functionality no matter the device or input method used. For instance, if you use a touch device, you will hardly use the keyboard, let alone a hotkey. But you will always be able to tap the button to perform the action.

Nevertheless there are exceptions where hotkeys can be convenient but flooding the form with triggers for, maybe non essential, actions would make the form cluttered more than needed.

πŸ‘‰ This is the case of the βž– and βž• buttons surrounding every phone number field in the previous examples which allowed to cherry pick the position where to remove or add a new phone: For small devices would be enough with the general βž– and βž• buttons that removes or adds a phone number from/to the end of the list.

πŸ’‘ In this scenario we can use CSS to hide the triggers while keeping them accessible through their hotkeys.

Keep in mind that if, like in our examples, you use a ::before (or ::after) pseudo-element to show the hotkey hint, you shouldn’t use a property that completely removes it from the DOM, like display: none;, since it will also prevent the ::before or ::after pseudo-element from appearing too.

Better use visibility: hidden; or opacity: 0; to hide the button and width: 0px; and/or height: 0px; as needed to prevent them from taking space in the layout.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
    <div data-smark='{"type": "list", "name": "phonelist", "sortable": true}'>
        <fieldset>
            <legend>
                <span data-smark='{"action":"removeItem", "hotkey":"-"}' title='Delete this phonebook entry' style='cursor:pointer'>[βž–]</span>
                <strong>
                    Contact
                    <span data-smark='{"action":"position"}'>N</span>
                </strong>
            </legend>            <p>
                <label data-smark='{"type": "label"}'>Name:</label>
                <input name='name' data-smark='{"type": "input"}' />
            </p>
            <p>
                <label data-smark='{"type": "label"}'>Surname:</label>
                <input name='surname' data-smark='{"type": "input"}' />
            </p>
            <button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>🧹</button>
            <button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>βž–</button>
            <button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>βž• </button>
            <strong data-smark="label">Phones:</strong>
            <ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
                <li class="row">
                    <label data-smark>πŸ“ž Telephone
                    <span data-smark='{"action":"position"}'>N</span>
                    </label>
                    <button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>βž–</button>
                    <input type="tel" data-smark>
                    <button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>βž• </button>
                </li>
            </ul>        </fieldset>
    </div>
    <p style="text-align: right; margin-top: 1em">
        <b>Total entries:</b>
        <span data-smark='{"action":"count", "context": "phonelist"}'>M</span>
    </p>
    <button
        data-smark='{"action":"addItem","context":"phonelist","hotkey":"+"}'
        style="float: right; margin-top: 1em"
    >βž• Add Contact</button>

</div>
#myForm li.row button[data-smark] {
    visibility: hidden;
    width: 0px;
    pointer-events: none;
}
#myForm li.row button[data-smark]::before {
    visibility: visible;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
    list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
    opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"));

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

This is just a simple trick and not any new SmarkForm feature, but it is worth to mention it here since it helps to build smoother and cleaner forms.

If you try to fill the former example you’ll notice that, when hitting the Ctrl key, the β€œ+” and β€œ-β€œ hotkey hints are shown beside the position of the, now hidden, βž• and βž– buttons.

…And, at the same time, the ones still visible in the outer context will allow touch device users to add or remove phone numbers even only to/from the end of the list.

Animations

SmarkForm is markup-agnostic and deliberately provides no built-in animation engine β€” transitions are a design concern that belongs to your CSS.

The technique is straightforward: use SmarkForm’s lifecycle events to add and remove CSS classes on list items, and let CSS transition do the rest.

  • afterRender fires after a new item’s DOM node has been inserted. Add an initial CSS class that hides or offsets the element, then β€” after a minimal delay to let the browser paint the initial state β€” add a second class that transitions it to its final visible position.

  • beforeUnrender fires before an item is removed from the DOM. Remove the β€œvisible” class and return a Promise that resolves after the transition duration. SmarkForm awaits that promise, so the element stays in the document long enough for the exit animation to complete.

πŸš€ Because both handlers filter by ev.context.parent?.options.type, a single pair of listeners covers every list in the form β€” including nested ones β€” with no per-list wiring required.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
    <div data-smark='{"type": "list", "name": "phonelist", "sortable": true}'>
        <fieldset>
            <legend>
                <span data-smark='{"action":"removeItem", "hotkey":"-"}' title='Delete this phonebook entry' style='cursor:pointer'>[βž–]</span>
                <strong>
                    Contact
                    <span data-smark='{"action":"position"}'>N</span>
                </strong>
            </legend>            <p>
                <label data-smark='{"type": "label"}'>Name:</label>
                <input name='name' data-smark='{"type": "input"}' />
            </p>
            <p>
                <label data-smark='{"type": "label"}'>Surname:</label>
                <input name='surname' data-smark='{"type": "input"}' />
            </p>
            <button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>🧹</button>
            <button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>βž–</button>
            <button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>βž• </button>
            <strong data-smark="label">Phones:</strong>
            <ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
                <li class="row">
                    <label data-smark>πŸ“ž Telephone
                    <span data-smark='{"action":"position"}'>N</span>
                    </label>
                    <button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>βž–</button>
                    <input type="tel" data-smark>
                    <button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>βž• </button>
                </li>
            </ul>        </fieldset>
    </div>
    <p style="text-align: right; margin-top: 1em">
        <b>Total entries:</b>
        <span data-smark='{"action":"count", "context": "phonelist"}'>M</span>
    </p>
    <button
        data-smark='{"action":"addItem","context":"phonelist","hotkey":"+"}'
        style="float: right; margin-top: 1em"
    >βž• Add Contact</button>

</div>
.animated_item {
    transform: translateX(-100%); /* Start off-screen to the left */
    opacity: 0; /* Optional: Start invisible for smoother effect */
    /* Transition for removal effect */
    transition: 
        transform 200ms ease-out,
        opacity 200ms ease-out;
}

.animated_item.ongoing {
    transform: translateX(0); /* End at original position */
    opacity: 1; /* Optional: Fully visible */
    transition: 
        transform 200ms ease-in,
        opacity 200ms ease-in;
}

#myForm li.row button[data-smark] {
    visibility: hidden;
    width: 0px;
    pointer-events: none;
}
#myForm li.row button[data-smark]::before {
    visibility: visible;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
    list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
    opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"));const delay = ms=>new Promise(resolve=>setTimeout(resolve, ms));
myForm.onAll("afterRender", async function(ev) {
    if (ev.context.parent?.options.type !== "list") return; /* Only for list items */
    const item = ev.context.targetNode;
    item.classList.add("animated_item");
    await delay(1); /* Important: Allow DOM to update */
    item.classList.add("ongoing");
});
myForm.onAll("beforeUnrender", async function(ev) {
    if (ev.context.parent?.options.type !== "list") return; /* Only for list items */
    const item = ev.context.targetNode;
    item.classList.remove("ongoing");
    /* Await for transition to be finished before item removal: */
    await delay(150);
});

Why add animated_item via JavaScript instead of directly in the HTML?

If the class were baked into the template, every item would start hidden even when JavaScript is unavailable. Adding it through the afterRender handler ensures the animation only kicks in when JS is active, so the form degrades gracefully without it.


Why the 1 ms delay in afterRender?

CSS transitions only fire when a property changes after the element is already in the document. If both animated_item and ongoing were added in the same task, the browser would never observe the initial hidden state and the transition would not play. The await delay(1) yields control for one event-loop tick, giving the rendering engine a chance to paint the initial state before ongoing is applied.


Why await delay(150) in beforeUnrender?

SmarkForm awaits the return value of beforeUnrender handlers before detaching the element from the DOM. By returning a promise that resolves after 150 ms (matching the CSS transition-duration), we keep the element visible just long enough for the exit animation to finish.


Applying this globally vs. per-list

myForm.onAll() listens on all components in the form. The guard ev.context.parent?.options.type !== "list" skips anything that is not a direct child of a list β€” subforms, labels, buttons, etc. The result is that any list added anywhere in the form hierarchy is automatically animated without further wiring.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

The afterRender handler adds animated_item via JavaScript rather than embedding it directly in the HTML template. This ensures the animation class is only present when JavaScript is active, so the form degrades gracefully if JS is disabled.

The beforeUnrender handler does the reverse: it removes ongoing and returns a Promise delayed by 150 ms β€” matching the CSS transition duration β€” so SmarkForm holds the element in the DOM while the exit animation plays out.

Smart value coercion

SmarkForm automatically normalises imported values to match the expected type and shape of each field. This keeps your forms resilient to data-model changes and ensures that what you save is always clean and well-typed.

Scalar-to-array list coercion

When a list field receives a non-array value β€” a plain string, a number, or an object β€” it automatically wraps it in a single-item array. This is particularly useful for model migrations: if a field that used to hold a single email string is upgraded to accept a list of emails, old saved data continues to work without any transformation step.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
    <button data-smark='{"action":"removeItem","context":"email","preserve_non_empty":true}' title="Remove email">βž–</button>
    <button data-smark='{"action":"addItem","context":"email"}' title="Add email">βž•</button>
    <strong data-smark="label">Emails:</strong>
    <ul data-smark='{"type":"list","name":"email","of":"input","min_items":0}'>
        <li data-smark='{"role":"empty_list"}'>(No emails on record)</li>
        <li><input type="email" data-smark placeholder="name@example.com"></li>
    </ul>
</div>
#myForm ul {
    list-style: none;
    padding-left: 0;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "email": "alice@example.com" // Old data saved before upgrading to an array
}
});

πŸ‘‰ Scalar-to-array coercion: If you import a plain string instead of an array, SmarkForm automatically places it in a single-item list.

  • Click ⬇️ Export, change ["alice@example.com"] to just "alice@example.com" in the JSON playground editor below, then click ⬆️ Import β€” the single email is placed in the list automatically.
  • This mirrors the upgrade from a single-value field (e.g. "email") to a list field (e.g. "emails": [...] ).

πŸ‘‰ Empty items are not exported by default (controlled by exportEmpties).

  • Click βž• to add a blank item, then ⬇️ Export β€” the blank row will be absent from the output, keeping saved data clean.
  • Set "exportEmpties": true on the list to keep blank slots in the data (useful in draft-save workflows where you want to preserve the user’s position in the list).

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Type coercion for scalar fields

Fields with a specific HTML type automatically coerce values on both import and export:

  • <input type="number"> exports a JavaScript number (not a string), and accepts string representations on import (e.g. "28" β†’ 28).
  • <input type="date"> exports an ISO 8601 string (YYYY-MM-DD), and accepts compact strings (YYYYMMDD) and Date objects on import.
  • <input type="time"> exports HH:MM:SS and accepts HH:MM on import.
  • Any field exports null when empty, to explicitly signal β€œunknown or indifferent” rather than an empty string.

Adding {"encoding":"json"} to any <input> or <textarea> enables JSON round-trips: the field stores the value internally as a JSON string but exports it as a parsed JavaScript value (object, array, number, or null).

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
    <p>
        <label data-smark>Name:</label>
        <input type="text" name="name" data-smark>
    </p>
    <p>
        <label data-smark>Age:</label>
        <input type="number" name="age" min="0" max="150" data-smark>
    </p>
    <p>
        <label data-smark>Date of Birth:</label>
        <input type="date" name="dob" data-smark>
    </p>
    <p>
        <label data-smark>Metadata (JSON):</label>
        <textarea name="metadata" data-smark='{"encoding":"json"}'></textarea>
    </p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "name": "Alice",
    "age": "28",  // String instead of number, will be coerced to a number
    "dob": "19960315", // Correctly parsed as date.
    "metadata": { // Will be exported/imported as JSON
                  // If invalid exports null (catch that from validation)
        "subscribed": true,
        "tier": "premium"
    }
}
});

πŸ‘‰ Number coercion: The Age field is <input type="number">.

  • SmarkForm always exports its value as a JavaScript number, not a string.
  • It also accepts string representations on import β€” try clicking ⬇️ Export, changing "age": 28 to "age": "28" (quoted) in the JSON playground editor, and clicking ⬆️ Import: the exported result will be "age": 28 (unquoted) again.

πŸ‘‰ Date normalization: The Date of Birth field is <input type="date">.

  • SmarkForm always exports an ISO 8601 string (YYYY-MM-DD).
  • It accepts compact strings (YYYYMMDD) and Date objects on import. Try clicking ⬇️ Export, changing "dob": "1996-03-15" to "dob": "19960315" in the JSON playground editor, and clicking ⬆️ Import β€” it will be normalised to "1996-03-15" on the next export.

πŸ‘‰ JSON encoding: The Metadata textarea has {"encoding":"json"}.

  • On import, an object or array is serialised to JSON text (pretty-printed in textareas for readability).
  • On export, the textarea content is parsed back into a JavaScript value β€” your saved data contains a real object, not a raw JSON string.
  • Works with any valid JSON: objects, arrays, numbers, booleans, and null.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Dynamic Dropdown Options

Section still under construction…

In this example, we’ll illustrate how to create dropdown menus with dynamic options. This is particularly useful for forms that need to load options based on user input or external data sources.

🚧 Missing Example 🚧

This section is still under construction and this example is not yet available.

Example id: .

πŸ™ Thank you for your patience.

Random Examples

Here are some random examples to showcase the flexibility of SmarkForm and how it can be used to create various types of forms or even more complex interfaces with different functionalities.

Simple Calculator

The following example implements a simple calculator with just single input field and several buttons triggering the import action over that field with the data property accordingly set.

It leverages the singleton pattern to avoid specifying the context for every button. Then a very simple JavaScript code makes the rest…

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
    <div class="calculator" data-smark='{"type": "input", "name": "display"}'>
        <!-- Using singleton pattern here allows us to avoid specifying the context for every button -->
        <input
            data-smark
            type="text"
            class="display"
            value="0"
            pattern="[0-9+\-*\/\(\).]+"
        >
        <div class="buttons">
            <button
                data-smark='{"action": "import", "data": "C", "hotkey": "c"}'
                class="clear"
            >C</button>
            <button
                data-smark='{"action": "import", "data": "("}'
            >(</button>
            <button
                data-smark='{"action": "import", "data": ")"}'
            >)</button>
            <button
                data-smark='{"action": "import", "data": "/", "hotkey": "/"}'
                class="operator"
            >Γ·</button>
            <button
                data-smark='{"action": "import", "data": "7"}'
            >7</button>
            <button
                data-smark='{"action": "import", "data": "8"}'
            >8</button>
            <button
                data-smark='{"action": "import", "data": "9"}'
            >9</button>
            <button
                data-smark='{"action": "import", "data": "*", "hotkey": "*"}'
                class="operator"
            >Γ—</button>
            <button
                data-smark='{"action": "import", "data": "4"}'
            >4</button>
            <button
                data-smark='{"action": "import", "data": "5"}'
            >5</button>
            <button
                data-smark='{"action": "import", "data": "6"}'
            >6</button>
            <button
                data-smark='{"action": "import", "data": "-", "hotkey": "-"}'
                class="operator"
            >-</button>
            <button
                data-smark='{"action": "import", "data": "1"}'
            >1</button>
            <button
                data-smark='{"action": "import", "data": "2"}'
            >2</button>
            <button
                data-smark='{"action": "import", "data": "3"}'
            >3</button>
            <button
                data-smark='{"action": "import", "data": "+", "hotkey": "+"}'
                class="operator"
            >+</button>
            <button
                data-smark='{"action": "import", "data": "0"}'
            >0</button>
            <button
                data-smark='{"action": "import", "data": "."}'
            >.</button>
            <button
                data-smark='{"action": "import", "data": "Del"}'
            >←</button>
            <button
                data-smark='{"action": "import", "data": "=", "hotkey": "Enter"}'
                class="equals"
            >=</button>
        </div>
    </div>

</div>
#myForm .calculator {
    background-color: #333;
    border-radius: 10px;
    padding: 20px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
    width: 300px;
}
#myForm .display {
    width: 100%;
    box-sizing: border-box;
    background-color: #fff;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
    margin-bottom: 10px;
    font-size: 24px;
    text-align: right;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
#myForm .display:invalid {
    background-color: #fcc;
}
#myForm .buttons {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 5px;
}
#myForm button {
    padding: 15px;
    font-size: 18px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    background-color: #555;
    color: white;
    transition: background-color 0.2s;
}
#myForm button:hover {
    background-color: #777;
}
#myForm .operator {
    background-color: #f9a825;
}
#myForm .operator:hover {
    background-color: #ffb300;
}
#myForm .equals {
    background-color: #4caf50;
}
#myForm .equals:hover {
    background-color: #66bb6a;
}
#myForm .clear {
    background-color: #d32f2f;
}
#myForm .clear:hover {
    background-color: #ef5350;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"));const invalidChars = /[^0-9+\-*\/().]+/g;

myForm.on("BeforeAction_import", async (ev)=>{
    const prevValue = await ev.context.export();
    const key = ev.data;
    switch (key) {
        case "C":
            ev.data = "0"; /* Clear display */
            break;
        case "Del":
            ev.data = prevValue.slice(0, -1) || "0"; /* Remove last character */
            break;
        case "=":
            try {
                /* Evaluate expression */
                const sanitized = prevValue.replace(invalidChars, '');
                ev.data = eval(sanitized);
            } catch (e) {
                alert("Invalid expression");
                ev.preventDefault(); /* Keep existing data */
            }
            break;
        default:
            if (prevValue.trim() === "0") {
                ev.data = key; /* Replace 0 with new input */
            } else {
                ev.data = prevValue + key; /* Append to existing value */
            };
    };
});

πŸ‘‰ The code in this example is listening to all import actions in the whole form.

This isn’t an issue for this simple example. But if we had other fields in the form (unless they were intended to be additional calculators) would be affected too.

In that case, we could have attached the listener directly to the display field like this:

myForm.onRendered(()=>{
    /* Now display field is rendered */
    const display = myForm.find("/display");
    display.onLocal("BeforeAction_import", async (ev)=>{
        /* ... */
    });
});

πŸ‘‰ Using .on()or .onLocal() here is indifferent since inputs have no children.

…But in case of forms (or lists of forms) using .on() would have lead to intercept every β€œBeforeAcction_import” event in it or its children while .onLocal() will only intercept those triggered by the form itself. Not from any of its descendants.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Notice that this calculator has the power superpower for free:

Expressions like 2**10 are valid, so you can calculate any power.

πŸ‘‰ A single event handler over the onAfterAction_import does all the magic by intercepting the new value and appending it to the current one except for the few special cases like C, Del and = where the value is handled accordingly.

Check the JS tab to see the little JavaScript code that does the job.

Don’t miss the Notes tab too for some additional insights.

πŸ‘Œ The best thing is that you can either use the calculator buttons or directly type in the input field: Every time you use a button, the import action will bring the focus back to the input field so you can continue typing.

Calculator (UX improved)

The UX feeling of the previous example isn’t perfect since it was intended to be a very simple implementation.

Let’s handle the keydown event too and notice the so little effort is needed to reach a perfect UX.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
    <div class="calculator" data-smark='{"type": "input", "name": "display"}'>
        <!-- Using singleton pattern here allows us to avoid specifying the context for every button -->
        <input
            data-smark
            type="text"
            class="display"
            value="0"
            pattern="[0-9+\-*\/\(\).]+"
        >
        <div class="buttons">
            <button
                data-smark='{"action": "import", "data": "C"}'
                class="clear"
            >C</button>
            <button
                data-smark='{"action": "import", "data": "("}'
            >(</button>
            <button
                data-smark='{"action": "import", "data": ")"}'
            >)</button>
            <button
                data-smark='{"action": "import", "data": "/"}'
                class="operator"
            >Γ·</button>
            <button
                data-smark='{"action": "import", "data": "7"}'
            >7</button>
            <button
                data-smark='{"action": "import", "data": "8"}'
            >8</button>
            <button
                data-smark='{"action": "import", "data": "9"}'
            >9</button>
            <button
                data-smark='{"action": "import", "data": "*"}'
                class="operator"
            >Γ—</button>
            <button
                data-smark='{"action": "import", "data": "4"}'
            >4</button>
            <button
                data-smark='{"action": "import", "data": "5"}'
            >5</button>
            <button
                data-smark='{"action": "import", "data": "6"}'
            >6</button>
            <button
                data-smark='{"action": "import", "data": "-"}'
                class="operator"
            >-</button>
            <button
                data-smark='{"action": "import", "data": "1"}'
            >1</button>
            <button
                data-smark='{"action": "import", "data": "2"}'
            >2</button>
            <button
                data-smark='{"action": "import", "data": "3"}'
            >3</button>
            <button
                data-smark='{"action": "import", "data": "+"}'
                class="operator"
            >+</button>
            <button
                data-smark='{"action": "import", "data": "0"}'
            >0</button>
            <button
                data-smark='{"action": "import", "data": "."}'
            >.</button>
            <button
                data-smark='{"action": "import", "data": "Backspace"}'
            >←</button>
            <button
                data-smark='{"action": "import", "data": "="}'
                class="equals"
            >=</button>
        </div>
    </div>

</div>
#myForm .calculator input.display  {
    caret-color: transparent; /* Hide caret */
}
#myForm .calculator {
    background-color: #333;
    border-radius: 10px;
    padding: 20px;
    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
    width: 300px;
}
#myForm .display {
    width: 100%;
    box-sizing: border-box;
    background-color: #fff;
    border: 1px solid #ccc;
    border-radius: 5px;
    padding: 10px;
    margin-bottom: 10px;
    font-size: 24px;
    text-align: right;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}
#myForm .display:invalid {
    background-color: #fcc;
}
#myForm .buttons {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 5px;
}
#myForm button {
    padding: 15px;
    font-size: 18px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    background-color: #555;
    color: white;
    transition: background-color 0.2s;
}
#myForm button:hover {
    background-color: #777;
}
#myForm .operator {
    background-color: #f9a825;
}
#myForm .operator:hover {
    background-color: #ffb300;
}
#myForm .equals {
    background-color: #4caf50;
}
#myForm .equals:hover {
    background-color: #66bb6a;
}
#myForm .clear {
    background-color: #d32f2f;
}
#myForm .clear:hover {
    background-color: #ef5350;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
  content:"Ctrl+" attr(data-hotkey);
  position:absolute; top:-1.6em; left:0;
  font-size:0.7em;
  background:#333; color:#fff;
  padding:1px 4px; border-radius:3px;
  white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"));var invalidChars = /[^0-9+\-*\/().]+/g;

function updateDisplay(prevValue, key) {
    switch (key.toLowerCase()) {
        case "c":
        case "delete":
            return "0"; /* Clear display */
            break;
        case "backspace":
            return prevValue.slice(0, -1) || "0"; /* Remove last character */
            break;
        case "=":
        case "enter": /* Keyboard enter key */
            try {
                /* Evaluate expression */
                const sanitized = prevValue.replaceAll(invalidChars, '');
                return eval(sanitized);
            } catch (e) {
                return "Error!";
            }
            break;
        default:
            if (!! key.match(invalidChars)) {
                return prevValue; /* Keep existing data */
            };
            if (prevValue.replace(/[0\s]+/, "") === "") {
                return key; /* Replace 0 with new input */
            };
            return prevValue + key; /* Append to existing value */
    };
};

myForm.on("BeforeAction_import", async (ev)=>{
    const prevValue = await ev.context.export();
    const key = ev.data;
    ev.data = updateDisplay(prevValue, key);
});

myForm.on("keydown", async (ev)=>{
    ev.preventDefault();
    const prevValue = await ev.context.export();
    const key = ev.originalEvent.key;
    const data = updateDisplay(prevValue, key);
    await ev.context.import(data);
});

πŸ•΅οΈ It’s off-topic but worth to mention the trick of doing !! key.match(invalidChars)) instead of invalidChars.test(key) is not arbitrary since invalidChars is a regex with the global flag set, which makes it suitable for β€˜String.replaceAll()’.

With test(), the internal lastIndex property won’t be reset making it to fail after first usage.

The !! bit is just stylistic to note we want to evaluate the result of .match() as a boolean.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

In this example we no longer need to define hotkeys since we are directly listening to all keydown events.

If you check the JS tab you’ll see that we extracted the key processing logic to a function called updateDisplay() that receives thwo arguments (prevValue and key) to calculate the new value of the display.

It returns null for invalid keystrokes and can report the β€œError!” condition directly to the display (like a real calculator) since it will be cleared with the next keystroke (no matter which event it comes from).

Then the BeforeAction_import event handler just calls that function and sets the ev.data property with its result.

The keydown event handler does call the updateDisplay() function but:

  • It takes the key from the original keydown event.
  • Calls the .preventDefault() method to avoid the keystroke effectively reaching the display.
  • Programmatically triggers the import action over the display with the new value calculated.

Since now all keyboard strokes are processed by to updateDisplay() function, this allows us to define handy aliases which will feel more natural in a PC keyboard for some keys like:

  • Enter as an alias for =
  • Delete as an alias for C

In the case of the formerly named Del key, we just renamed it to Backspace to match the real key since Del was just a random name to void using en Emojii (←) as a key name.

We also added a little CSS rule to hide the caret in the input field since the display will no longer be directly editable.

Team Event Planner

This is the same demo shown on the πŸ”— landing page β€” a compact form that showcases several SmarkForm features at once: a nested subform, a sortable variable-length list, context-driven hotkeys, and date/time coercion.

Use the JSON editor below to inspect the exported data as you interact with the form, or import your own JSON to pre-populate it.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
  <div class="ep">
    <p>
      <label data-smark>πŸ“‹ Event:</label>
      <input data-smark name="title" type="text" placeholder="e.g. Sprint Review">
    </p>
    <p>
      <label data-smark>πŸ“… Date:</label>
      <input data-smark name="date" type="date">
    </p>
    <p>
      <label data-smark>⏰ Time:</label>
      <input data-smark name="time" type="time">
    </p>
    <fieldset data-smark='{"type":"form","name":"organizer"}'>
      <legend data-smark='label'>πŸ‘€ Organizer</legend>
      <p>
        <label data-smark>Name:</label>
        <input data-smark name="name" type="text">
      </p>
      <p>
        <label data-smark>Email:</label>
        <input data-smark name="email" type="email">
      </p>
    </fieldset>
    <div class="ep-list">
      <button data-smark='{"action":"removeItem","context":"attendees","hotkey":"Delete","preserve_non_empty":true}' title='Remove empty slots'>🧹</button>
      <button data-smark='{"action":"addItem","context":"attendees","hotkey":"+"}' title='Add attendee'>βž•</button>
      <strong data-smark='label'>πŸ‘₯ Attendees:</strong>
      <ul data-smark='{"type":"list","name":"attendees","sortable":true,"exportEmpties":false}'>
        <li>
          <details>
            <summary>
              <span data-smark='{"type":"label"}' class="bullet">
                <span data-smark='{"action":"position"}'>N</span> ☰
              </span>
              <input data-smark type="text" name="name" placeholder="Name">
              <button data-smark='{"action":"removeItem","hotkey":"-"}' title='Remove'>βž–</button>
              <button data-smark='{"action":"addItem","hotkey":"+"}' title='Insert here'>βž•</button>
            </summary>
            <div class="ep-attendee">
              <input data-smark type="email" name="email" placeholder="Email">
              <input data-smark type="tel" name="phone" placeholder="Phone">
            </div>
          </details>
        </li>
      </ul>
    </div>
    <p class="ep-hint">πŸ’‘ Hold <kbd>Ctrl</kbd> to reveal shortcuts</p>
  </div>
</div>
#myForm .ep {
    display: flex;
    flex-direction: column;
    gap: 0.35em;
    max-width: 460px;
    font-size: 0.95em;
}
#myForm .ep p {
    display: flex;
    align-items: center;
    gap: 0.5em;
    margin: 0;
}
#myForm .ep label {
    font-weight: 500;
    white-space: nowrap;
}
#myForm .ep label:not(.bullet) {
    min-width: 4.5em;
}
#myForm .ep input {
    padding: 0.3em 0.5em;
    border: 1px solid #ccc;
    border-radius: 4px;
}
#myForm .ep input[type="text"],
#myForm .ep input[type="email"] {
    flex: 1;
}
#myForm .ep fieldset {
    border: 1px solid #ddd;
    border-radius: 6px;
    padding: 0.4em 0.8em 0.6em;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.3em;
}
#myForm .ep fieldset legend {
    font-weight: bold;
    padding: 0 0.3em;
}
#myForm .ep-list ul {
    list-style: none;
    padding: 0;
    margin: 0.2em 0 0;
    display: flex;
    flex-direction: column;
    gap: 0.25em;
}
#myForm .ep-list ul li {
    display: flex;
    align-items: flex-start;
    gap: 0.3em;
}
#myForm .ep-list ul li details {
    width: 100%;
    border: 1px solid transparent;
    border-radius: 4px;
    transition: border-color 0.15s;
}
#myForm .ep-list ul li details[open] {
    border-color: #ccc;
    padding-bottom: 4px;
}
#myForm .ep-list ul li summary {
    display: flex;
    align-items: center;
    gap: 0.4em;
    cursor: default;
    user-select: none;
    padding: 0.1em 0.2em;
    list-style: none;
}
#myForm .ep-attendee {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4em;
    padding: 0.3em 0.4em 0.1em 1.5em;
}
#myForm .ep-attendee input {
    flex: 1;
    min-width: 120px;
}
#myForm .ep-hint {
    font-size: 0.82em;
    color: #888;
    margin: 0.15em 0 0;
}
#myForm .ep-hint kbd {
    background: #f4f4f4;
    border: 1px solid #ccc;
    border-radius: 3px;
    padding: 1px 4px;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
    content:"Ctrl+" attr(data-hotkey);
    position:absolute; top:-1.6em; left:0;
    font-size:0.7em;
    background:#333; color:#fff;
    padding:1px 4px; border-radius:3px;
    white-space:nowrap;
}
/* Attendee list item entry/exit animations */
#myForm .ep-list ul li.animated_item {
    transform: translateX(-100%);
    opacity: 0;
    transition: transform 200ms ease, opacity 200ms ease;
}
#myForm .ep-list ul li.animated_item.ongoing {
    transform: translateX(0);
    opacity: 1;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "title": "Sprint Review",
    "date": "2025-03-15",
    "time": "10:00:00",
    "organizer": {
        "name": "Alice Johnson",
        "email": "alice@example.com"
    },
    "attendees": [
        {"name": "Bob Smith",   "email": "bob@example.com",   "phone": "+1 555 200 0001"},
        {"name": "Carol White", "email": "carol@example.com", "phone": "+1 555 200 0002"},
        {"name": "Dave Brown",  "email": "dave@example.com",  "phone": "+1 555 200 0003"}
    ]
}
});const delay = ms=>new Promise(resolve=>setTimeout(resolve, ms));
myForm.onAll("afterRender", async function(ev) {
    if (ev.context.parent?.options.type !== "list") return;
    const item = ev.context.targetNode;
    item.classList.add("animated_item");
    await delay(1);
    item.classList.add("ongoing");
});
myForm.onAll("beforeUnrender", async function(ev) {
    if (ev.context.parent?.options.type !== "list") return;
    const item = ev.context.targetNode;
    item.classList.remove("ongoing");
    await delay(150);
});

πŸ‘‰ This demo highlights several SmarkForm features at once:

  • Foldable rows: Each attendee row uses a native <details>/<summary> element. Click the β–Ά triangle (or anywhere on the row header outside the name field and action buttons) to expand or collapse the extra fields. A <span data-smark='{"type":"label"}'> inside the <summary> acts as the SmarkForm label, making it the drag handle for reordering (shown as ☰). Because the <summary> itself is not the label, the native disclosure triangle and fold/unfold on click are preserved without extra CSS workarounds.
  • Nested subform: The organizer fieldset is a subform β€” its fields are grouped and exported as a nested object.
  • Sortable list: Attendees can be dragged to reorder them. The list uses exportEmpties: false so empty slots are not exported.
  • Context-driven hotkeys: The βž•/βž– buttons inside each list item carry -/+ hotkeys, active only when focus is within that item. The 🧹 button uses Delete as a context-wide hotkey.
  • Date & time coercion: The date and time inputs use SmarkForm’s built-in type coercion β€” values are normalised to ISO date/time format on import/export.
  • Label components: data-smark='label' on non-<label> elements and bare data-smark on <label> elements wire labels to their fields automatically.
  • In-form hint: The πŸ’‘ Hold Ctrl to reveal shortcuts text lives inside the form itself rather than as external documentation.
  • Attendee animations: Items slide in and out via afterRender / beforeUnrender event handlers that toggle CSS classes β€” no animation library required.

Every example in this section comes with many of the following tabs:

  • HTML: HTML source code of the example.
  • CSS: CSS applied (if any).
  • JS: JavaScript source code of the example.
  • Preview: Live, sandboxed rendering of the example β€” fully isolated from the page styles.
  • Notes: Additional notes and insights for better understanding. Don't miss it‼️

✨ In the Preview tab, a JSON playground editor is available with handy buttons:

  • ⬇️ Export to export the form data to the JSON playground editor.
  • ⬆️ Import to import data from the JSON playground editor into the form.
  • ♻️ Reset to reset the form to its default values.
  • ❌ Clear to clear the whole form.

πŸ’‘ The JSON playground editor is part of the SmarkForm form itself β€” it is just omitted from the code snippets to keep the examples focused on what matters.

πŸ› οΈ Between the tab labels and the content there is always an edit toolbar:

  • ✏️ Edit β€” activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β€” the original example is not affected.
  • πŸ“‹ Include playground editor β€” (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.
  • ▢️ Run β€” (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.

Conclusion

Section still under construction…

We hope these examples have given you a good overview of what SmarkForm can do. By leveraging the power of markup-driven forms, SmarkForm simplifies the creation of interactive and intuitive forms, allowing you to focus on your application’s business logic. Feel free to experiment with these examples and adapt them to suit your specific needs.

For more detailed information and documentation, please refer to the other sections of this manual. If you have any questions or need further assistance, don’t hesitate to reach out to the SmarkForm community.

Happy form building!