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

Simple plain form

To begin with the basics, we’ll start with a simple form that includes a few input fields.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
<h2>Model details</h2>
    <p>
        <label data-smark>Model Name:</label>
        <input type="text" name="model" data-smark />
    </p>
    <p>
        <label data-smark>Type:</label>
        <select name="type" data-smark='{"encoding":"json"}'>
            <option value='null'>πŸ‘‡ Please select...</option>
            <!-- json encoding allow us return null values -->
            <option value='"Car"'>Car</option>
            <!-- ...but now we must wrap strings in double quotes -->
            <!-- (it also gives us the ability to return objects and arrays) -->
            <option>Bicycle</option>
            <!-- ...but if we are Ok with inner text as value, we can just omit the value attribute -->
            <option>Motorcycle</option>
            <option>Van</option>
            <option>Pickup</option>
            <option>Quad</option>
            <option>Truck</option>
        </select>
    </p>
    <p>
        <label data-smark>Detailed description:</label>
        <textarea name="longdesc" data-smark ></textarea>
    </p>
    <p>
        <label data-smark>Seats:</label>
        <input type="number" name="seats" min=4 max=9 data-smark />
    </p>
    <p>
        <label data-smark>Driving Side:</label>
        <input type="radio" name="side" value="left" data-smark /> Left
        <input type="radio" name="side" value="right" data-smark /> Right
    </p>
    <p>
        <label data-smark>Color:</label>
        <span data-smark='{"type":"color", "name":"color"}'>
            <input data-smark>
            <button data-smark='{"action":"clear"}' title='Indifferent or unknown' >❌ </button>
        </span>
    </p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));

πŸ‘‰ Notice that most SmarkForm fields can be null, to explicitly mean that the information is unknown or indifferent.

  • In the case of radio buttons, if no option is selected, they evaluate to null.
    • Even after a value is set, they allow unselectiong the selected option either by clicking on it again or by pressing the Delete key.
  • Even color pickers can be null even native HTML color inputs can’t.
    • Just press the Deletekey or use the ❌ button to call it’s β€œclear” action.

πŸ‘‰ This kind of SmarkForm components intended to call actions on SmarkForm fields are called triggers.

  • There are several other actions that can be called on SmarkForm fields. Some, such as import and export are common to all field types and others are specific to some of them. For instance addItem and removeItem are specific to lists.

πŸ‘‰ Also notice the {"encoding":"json"} bit in the <select> dropdown.

  • This allow it to return a Null value when the first option is selected.
  • It also forces to wrap other values in double quotes to make them valid JSON strings.
  • …unless the value property is omitted, in which case inner text is used β€œas is”.

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 everything works with no JS code other than SmarkForm instantiation itself (I swear).

For instance, you can:

  • Type some data in the form.
  • Export it to the textarea in JSON format.
  • Clear the form whenever you want.
  • Edit the JSON as you like.
  • Import the JSON back to the form.
  • See the effects of your changes.

Nested forms

Let’s add a few more fields to the form to provide information regarding included safety equipment. This time we’ll group them in a nested subform under the name β€œsafety”.

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
❓
<div id="myForm">
<h2>Model details</h2>
    <p>
        <label data-smark>Model Name:</label>
        <input type="text" name="model" data-smark />
    </p>
    <p>
        <label data-smark>Type:</label>
        <select name="type" data-smark='{"encoding":"json"}'>
            <option value='null'>πŸ‘‡ Please select...</option>
            <!-- json encoding allow us return null values -->
            <option value='"Car"'>Car</option>
            <!-- ...but now we must wrap strings in double quotes -->
            <!-- (it also gives us the ability to return objects and arrays) -->
            <option>Bicycle</option>
            <!-- ...but if we are Ok with inner text as value, we can just omit the value attribute -->
            <option>Motorcycle</option>
            <option>Van</option>
            <option>Pickup</option>
            <option>Quad</option>
            <option>Truck</option>
        </select>
    </p>
    <p>
        <label data-smark>Detailed description:</label>
        <textarea name="longdesc" data-smark ></textarea>
    </p>
    <p>
        <label data-smark>Seats:</label>
        <input type="number" name="seats" min=4 max=9 data-smark />
    </p>
    <p>
        <label data-smark>Driving Side:</label>
        <input type="radio" name="side" value="left" data-smark /> Left
        <input type="radio" name="side" value="right" data-smark /> Right
    </p>
    <p>
        <label data-smark>Color:</label>
        <span data-smark='{"type":"color", "name":"color"}'>
            <input data-smark>
            <button data-smark='{"action":"clear"}' title='Indifferent or unknown' >❌ </button>
        </span>
    </p>
    <fieldset data-smark='{"name":"safety","type":"form"}'>
        <legend>Safety Features:</legend>
        <span>
            <label><input type="checkbox" name="airbag" data-smark /> Airbag.</label>
        </span>
        &nbsp;&nbsp;
        <span>
            <label><input type="checkbox" name="abs" data-smark /> ABS.</label>
        </span>
        &nbsp;&nbsp;
        <span>
            <label><input type="checkbox" name="esp" data-smark /> ESP.</label>
        </span>
        &nbsp;&nbsp;
        <span>
            <label><input type="checkbox" name="tc" data-smark />TC.</label>
        </span>
    </fieldset>
</div>
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.

Lists

One of the most powerful features of SmarkForm is its ability to handle variable-length lists.

Let’s say you need to collect phone numbers or emails from users. Instead of having (and dealing with it) a fixed number of input fields, you can use a list that can grow or shrink as needed:

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<div id="myForm">
  <button data-smark='{"action":"removeItem", "context":"phones"}' title='Remove phone number'>βž–</button>
  <button data-smark='{"action":"addItem","context":"phones"}' title='Add phone number'>βž• </button>
  <strong data-smark="label">Phones:</strong>
  <div data-smark='{"type":"list", "name": "phones", "of": "input", "exportEmpties": true}'>
    <input type="tel" style="display: block">
  </div>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
  • By default, empty items in lists are not expoted to keep data clean.
    • But for this very first example, we added the {exportEmpties: true} option so that you can see every added item no matter if you typed anything or not.

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.

Here we used a simple <input> field for each item in the list and had to trick them with style="display: block;" to make them to stack gracefully.

But lists are even more powerful than that…

For instance, we could have used a form field instead, but in this case we would had got a JSON object for each item in the list, which is not what we want in this specific case.

πŸ‘‰ To address this issue, we can take advantage of the singleton pattern which allows us to make any HTML element to work as a regular input field.

We call the singleton pattern when we use any HTML element different from <input>, <select>, <textarea>, etc., as a regular SmarkForm field.

For this to work we only need define the data-smark property on it specifying the appropriate type and place one and only one of these elements (with the β€œdata-smark” attribute since otherwise they are ignored) in its contents.

This way we can not only use a more elaborated structure for each item in the list: It also allows us to include other controls within every list item, like in the following example:

πŸ”—
πŸ—’οΈ HTML
🎨 CSS
βš™οΈ JS
πŸ‘οΈ Preview
πŸ“ Notes
❓
<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, "min_items":0, "max_items":5}'>
    <li data-smark='{"role": "empty_list"}' class="row">(None)</li>
    <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>
#myForm ul li {
    list-style-type: none;
}
const myForm = new SmarkForm(document.getElementById("myForm"));

πŸ‘‰ In this example we:

  • Established a maximum of 5 items in the list.
  • Allowed the list to be empty (default minimum items is 1).
  • Defined an alternate template for the case of empty list.
  • Made the βž– button a little smarter so that it removes empty items, if any, first.
  • Added a 🧹 button to remove all empty items.
  • Prepended a βž– button to each item to cherry-pick which items to remove.
  • Appended a βž• button to each item to allow inserting items at a given position.
  • Returned to the default behaviour of not exporting empty items.
  • Made it sortable (by dragging and dropping items).
  • Also notice that when the max_items limit is reached, every addItem trigger, like the βž• button is automatically disabled.
  • …Same applies to removeItem triggers when the min_items limit is reached.

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 example may look a bit bloated, but it is just to show the power and flexibility of SmarkForm trigger components.

In a real application you will be able to pick those controls that best suit your needs and use them as you like.

πŸ‘‰ And, again, don’t miss to check the πŸ“ Notes tab for more powerful insights and tips.

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>
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
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…

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 class="schedule-row" data-smark='{"type":"list","name":"rcpt_schedule","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
      <strong data-smark='{"role":"header"}'>πŸ›ŽοΈ Reception:</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>
    <div class="schedule-row" data-smark='{"type":"list","name":"bar_schedule","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
      <strong data-smark='{"role":"header"}'>🍸 Bar</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>
    <div class="schedule-row" data-smark='{"type":"list","name":"restaurant_schedule","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
      <strong data-smark='{"role":"header"}'>🍽️ Restaurant:</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>
    <div class="schedule-row" data-smark='{"type":"list","name":"pool_schedule","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
      <strong data-smark='{"role":"header"}'>🏊 Pool:</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>
  </div>
</div>
#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;
  }
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
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.

Mixins

The .schedule-row pattern was repeated four times in the hotel example above β€” once for each service row. With Mixin Types, you define that pattern once inside a <template> element and reference it from as many usage sites as you need.

Beyond reuse, the <template> tag unlocks two companion features that can live as a direct sibling of the template root:

  • An optional <style> sibling β€” injected into <head> exactly once, regardless of how many times the mixin is used, keeping your page free of duplicate CSS.

  • An optional <script> sibling β€” that we’ll discuss later.

πŸ”—
πŸ—’οΈ 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>
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "schedules": {
        "rcpt_schedule": [
            {"start": "08:00:00", "end": "20:00:00"}
        ],
        "bar_schedule": [
            {"start": "11:00:00", "end": "23:00:00"}
        ],
        "restaurant_schedule": [
            {"start": "07:30:00", "end": "10:30:00"},
            {"start": "19:00:00", "end": "22:00:00"}
        ],
        "pool_schedule": [
            {"start": "09:00:00", "end": "18:00:00"}
        ]
    }
}
});

πŸ‘‰ The #scheduleRow template bundles markup and styles in one place:

  • The <style> sibling is injected into <head> once β€” it doesn’t matter how many times the mixin is used on the page; the same CSS block is never duplicated.

πŸ‘‰ Each usage site (placeholder) keeps its own identity:

  • name is supplied by the placeholder, not the template β€” each row gets its own field name and data path.
  • The <span data-for="label"> child inside each placeholder replaces the <span id="label"> slot in the template (which defaults to "Schedule"), so every row shows its own label without touching the template itself.
  • Any data-smark option in the placeholder overrides the template default β€” e.g. pass "max_items":5 to allow more intervals on a specific row.

πŸ‘‰ Templates are placed after the form for readability: the usage is visible first, and the definition follows.

πŸ‘‰ External templates:

  • In this example the <template> lives in the same document (local mixin, "type":"#scheduleRow").
  • You can equally point to a template in another file: "type":"./shared/widgets.html#scheduleRow". The external document is fetched once and all references share the same cached copy.

Tip: Use sufficiently unique CSS class names inside mixin <style> blocks β€” injected styles are global. The schedule-row / schtbl names used here mirror the plain-HTML example above, keeping the showcase consistent.

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? 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 data-smark="label">πŸ—“οΈ Periods:</h2>
  <div data-smark='{"type":"list","name":"periods","sortable":true,"exportEmpties":true}'>
    <fieldset 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":"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>
  </div>
  <button
    data-smark='{"action":"addItem","context":"periods","hotkey":"+"}'
    style="float: right; margin-top: 1em"
  >βž• Add Period</button>
</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;
  }
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "periods": [
        {
            "start_date": "2025-04-01",
            "end_date": "2025-09-30",
            "schedules": {
                "rcpt_schedule": [
                    {
                        "start": "07:00:00",
                        "end": "23:00:00"
                    }
                ],
                "bar_schedule": [
                    {
                        "start": "10:00:00",
                        "end": "23:00:00"
                    }
                ],
                "restaurant_schedule": [
                    {
                        "start": "07:00: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"
                    }
                ]
            }
        }
    ]
}
});

πŸ‘‰ The #scheduleRow template bundles markup and styles in one place:

  • The <style> sibling is injected into <head> once β€” it doesn’t matter how many times the mixin is used on the page; the same CSS block is never duplicated.

πŸ‘‰ Each usage site (placeholder) keeps its own identity:

  • name is supplied by the placeholder, not the template β€” each row gets its own field name and data path.
  • The <span data-for="label"> child inside each placeholder replaces the <span id="label"> slot in the template (which defaults to "Schedule"), so every row shows its own label without touching the template itself.
  • Any data-smark option in the placeholder overrides the template default β€” e.g. pass "max_items":5 to allow more intervals on a specific row.

πŸ‘‰ Templates are placed after the form for readability: the usage is visible first, and the definition follows.

πŸ‘‰ External templates:

  • In this example the <template> lives in the same document (local mixin, "type":"#scheduleRow").
  • You can equally point to a template in another file: "type":"./shared/widgets.html#scheduleRow". The external document is fetched once and all references share the same cached copy.

Tip: Use sufficiently unique CSS class names inside mixin <style> blocks β€” injected styles are global. The schedule-row / schtbl names used here mirror the plain-HTML example above, keeping the showcase consistent.

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.

πŸ‘‰ Notice that you can manually sort the periods in the list by dragging and dropping them.

Drag and Drop events are not natively supported by touch devices.

They can be emulated in several ways. A quite straightforward one is through the drag-drop-touch library from Bernardo Castilho:

⚑ Not yet implemented but, in a near future, SmarkForm lists will also support automatic sorting features that, in this case, would allow to automatically sort the periods by start date.

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.

On the other hand, if we need to allow the list to be empty, just setting min_items to 0 will cause that no item is presented by default which leads to poor usability.

To address these issues we can do the following:

  • To ease adding new items: Add a custom addItem trigger using the source property to duplicate an entry and just edit what’s different. To do so:
    • Use the source property in that addItem trigger so that the import action will be automatically called with its value passed as its target after the new item being rendered.
    • I.e. with data-smark='{"source":".-1"}, the new item will be prefilled with the data from its previous item in the list.
  • To support empty lists without hurting usability:
    • Allow the list to be empty by setting its min_items to 0.
    • Set the lists’s value property to an array with one empty item (we can use an empty object to allow item defaults).
    • I.e. data-smark='{"min_items":0,"value": [{}]}'.

Below is the same example as before, but with an additional ✨ button to duplicate the data from the previous one and the before mentioned tweaks to allow the list to be empty emptied even showing one initial item for better usability by default:

πŸ”—
πŸ—’οΈ 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>
    <fieldset 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>
  </div>
  <button
    data-smark='{"action":"addItem","context":"periods","hotkey":"+"}'
    style="float: right; margin-top: 1em"
  >βž• Add Period</button>
</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;
  }
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "value": {
    "periods": [
        {
            "start_date": "2025-01-01",
            "end_date": "2025-05-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_date": "2025-06-01",
            "end_date": "2025-09-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": "09:30:00",
                        "end": "19:30:00"
                    }
                ]
            }
        }
    ]
}
});

πŸ‘‰ Customize a period by adding different schedules for each service.

πŸ‘‰ Use the ✨ button (or press Ctrl+d) to duplicate that period and notice the newly created one is prefilled with the same data.

πŸ‘‰ Remove all pereiods and notice the πŸ”’ Out of Service message shown when the list is empty.

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.

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

Mixin templates can themselves reference other mixins β€” creating a composition chain where each level adds its own behaviour.

In the example below we introduce a second mixin, #periodItem, that wraps the entire period fieldset (including the embedded #scheduleRow rows).

  • The inner mixin (#scheduleRow) keeps its <style> β€” injected once for the whole page.
  • The outer mixin (#periodItem) adds a <script> sibling β€” a per-instance hook that registers a smart date-prefill handler on the parent periods list.

The script fires through AfterAction_addItem, which fires after any source import (used by the ✨ duplicate button). This guarantees the date logic always runs on the final data β€” whether the item is brand new or duplicated.

πŸ”—
πŸ—’οΈ 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;

    // Register on the parent 'periods' list.  Each period's script adds one
    // listener, but each listener only ever handles its own item (identified by
    // path) so accumulation is harmless β€” O(N) listeners, O(1) work each.
    item.parent.on('AfterAction_addItem', async function(ev) {

      // Only process the event for THIS specific period.
      if (!ev.data || ev.data.getPath() !== item.getPath()) return;

      // Find this item's 0-based position in the exported data.
      const idx   = parseInt(item.getPath().split('/').pop());
      // Export with empties so the newly added item appears in the array.
      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;

      // Format a Date as "YYYY-MM-DD" using LOCAL calendar fields so the result
      // is never shifted by the browser's UTC offset (toISOString() is UTC-based
      // and would produce the wrong date in any UTC+ timezone).
      const fmtDate = d =>
        d.getFullYear() + '-'
        + String(d.getMonth() + 1).padStart(2, '0') + '-'
        + String(d.getDate()).padStart(2, '0');

      // ── Start date ────────────────────────────────────────────────────────
      // Anchor start_date just after the previous period ends.
      // No previous period β†’ default to today.
      // Previous period without an end_date β†’ leave blank (no anchor available).
      let startDate = null;
      if (prev?.end_date) {
        const d = new Date(prev.end_date + 'T00:00:00');
        d.setDate(d.getDate() + 1);       // setDate handles month/year boundaries
        startDate = fmtDate(d);
      } else if (!prev) {
        startDate = fmtDate(new Date());
      }

      // ── End date ──────────────────────────────────────────────────────────
      let endDate = null;

      if (startDate && !next) {
        // Appended at the end β€” mirror the previous period's duration so the
        // user has a sensible default to adjust rather than a blank end date.
        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');

          // Month-aware: if the previous period starts on the 1st and ends on
          // the last day of a month it spans whole calendar months β†’ mirror
          // in months so the new period aligns with month boundaries too.
          const pStartsFirst = pStart.getDate() === 1;
          const pEndsLast    = pEnd.getDate() ===
            new Date(pEnd.getFullYear(), pEnd.getMonth() + 1, 0).getDate();

          if (pStartsFirst && pEndsLast) {
            // Count months from start to end (inclusive).
            const months =
              (pEnd.getFullYear()  - pStart.getFullYear()) * 12 +
              (pEnd.getMonth()     - pStart.getMonth()   ) + 1;
            // End on the last day of the Nth month from nStart.
            endDate = fmtDate(
              new Date(nStart.getFullYear(), nStart.getMonth() + months, 0)
            );
          } else {
            // Day-level: count days and apply the same span.
            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) {
        // Inserted between two existing periods.
        //
        // "Contiguous" case: next starts on the same day as our computed
        // start_date (no gap between prev.end and next.start).  The user
        // must decide the split point manually β†’ leave end_date blank.
        //
        // "Gap" case: next.start_date is later than our start_date, meaning
        // there are unassigned days between the two periods.  Fill the gap:
        // set end_date = the day before 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);
        }
        // Contiguous (next.start_date === startDate) β†’ endDate stays null.
      }

      // ── Conflict guard ────────────────────────────────────────────────────
      // Never propose an end date that predates the start date.
      if (endDate && startDate && endDate < startDate) endDate = null;

      // ── Write to fields ───────────────────────────────────────────────────
      // Always overwrite both fields (clears stale dates from a duplication).
      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;
  }
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
    "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) β€” provides the time-interval list with its fancy grid <style> injected once into <head>.
  • #periodItem (outer) β€” wraps the whole period fieldset and uses #scheduleRow inside it, demonstrating mixin composition.

πŸ‘‰ The <script> in #periodItem β€” smart date prefill:

  • Runs once per period component instance with this bound to the form component for that period.
  • Registers an AfterAction_addItem listener on the parent list. The listener fires after any source import (used by ✨), so it always operates on the final data β€” whether the item is brand new or duplicated. Stale dates from duplication are always overwritten.
  • Date logic applied:
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 (month-aware)
Inserted β€” gap exists prev.end + 1 day next.start βˆ’ 1 day
Inserted β€” contiguous prev.end + 1 day blank (user decides the split)
  • end_date is cleared whenever it would predate start_date.

πŸ‘‰ The <style> from #scheduleRow is only injected once even though it appears inside #periodItem which is instantiated multiple times.

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.

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>
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
/* 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>
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
/* 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.

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>
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
/* 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;
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
/* 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;
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
/* 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;
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
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;
}
/* Materialize hotkey hints from data-hotkey attribute */
#myForm [data-hotkey] {
  position: relative;
  overflow-x: visible;
}
#myForm [data-hotkey]::before {
  content: attr(data-hotkey);
  display: inline-block;
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 10;
  pointer-events: none;
  background-color: #ffd;
  color: #44f;
  outline: 1px solid lightyellow;
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: bold;
  font-family: sans-serif;
  font-size: 0.8em;
  white-space: nowrap;
  transform: scale(1.8) translate(0.1em, 0.1em);
}
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","of":"input","sortable":true,"exportEmpties":false}'>
        <li>
          <span data-smark='{"action":"position"}'>N</span>.
          <input data-smark type="text" placeholder="Name">
          <button data-smark='{"action":"removeItem","hotkey":"-"}' title='Remove'>βž–</button>
          <button data-smark='{"action":"addItem","hotkey":"+"}' title='Insert here'>βž•</button>
        </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 {
    min-width: 4.5em;
    font-weight: 500;
    white-space: nowrap;
}
#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: center;
    gap: 0.4em;
}
#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;
    overflow-x: visible;
}
#myForm [data-hotkey]::before {
    content: attr(data-hotkey);
    display: inline-block;
    position: absolute;
    top: 2px;
    left: 2px;
    z-index: 10;
    pointer-events: none;
    background-color: #ffd;
    color: #44f;
    outline: 1px solid lightyellow;
    padding: 2px 8px;
    border-radius: 4px;
    font-weight: bold;
    font-family: sans-serif;
    font-size: 0.8em;
    white-space: nowrap;
    transform: scale(1.8) translate(0.1em, 0.1em);
}
/* Attendee list item entry/exit animations */
#myForm .ep-list ul li.animated_item {
    transform: translateX(-100%);
    opacity: 0;
    transition:
        transform 200ms ease-out,
        opacity 200ms ease-out;
}
#myForm .ep-list ul li.animated_item.ongoing {
    transform: translateX(0);
    opacity: 1;
    transition:
        transform 200ms ease-in,
        opacity 200ms ease-in;
}
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": [
        "Bob Smith",
        "Carol White",
        "Dave Brown"
    ]
}
});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:

  • 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!