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
- Import and Export Data
- Advanced UX Improvements
- Random Examples
- Conclusion
Basics
Just a Form
Fields auto-register from HTML attributes β no JavaScript beyond initialization. Try editing values in the preview or clicking the β¬οΈ Export button to edit the JSON in the playground editor at the bottom.
<div id="myForm">
<p><label data-smark>Name:</label><input name="name" type="text" data-smark></p>
<p><label data-smark>Age:</label><input name="age" type="number" data-smark></p>
<p>
<label data-smark>Favorite Color:</label>
<span data-smark='{"type":"color","name":"color"}'>
<input data-smark><button data-smark='{"action":"clear"}' title="Clear">β</button>
</span>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {"name":"Alice","age":28,"color":"#6366f1"}
});
π Null values. SmarkForm fields can be null to mean βunknownβ. Unlike native HTML, even <input type="color"> can be null β just press Delete or use the β trigger.
π Triggers. Elements with data-smark='{"action":"..."}' call actions on SmarkForm fields. Common actions: import, export, clear, addItem, removeItem.
π JSON editor. The Editor tab shows the form data as JSON. Edit and click outside to import your changes back.
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
Three-Level Nesting
This single HTML example packs a surprising range of SmarkForm features: three-level nesting of forms and lists, sortable groups, cross-list drag-and-drop for students, nested collapsible sections, auto-disabling triggers that respect min_items/max_items boundaries, per-subject grade lists that grow on demand, empty-list placeholders, and automatic position numbering β all from the declarative HTML you see in the source tab with no custom JavaScript. Each capability is introduced step by step in the pages that follow.
<div id="myForm">
<div class="sg">
<div class="sg-header">
<input name="name" placeholder="School name" type="text" data-smark>
<input name="level" placeholder="Level (e.g. Primary)" type="text" data-smark>
<input name="year" placeholder="Academic year" type="text" data-smark>
</div>
<ul data-smark='{"type":"list","name":"groups","sortable":true,"min_items":0}'>
<li data-smark='{"role":"empty_list"}' class="sg-empty">No groups yet</li>
<li>
<div class="sg-card">
<details>
<summary>
<span data-smark='{"type":"label"}' class="sg-handle">β Ώ</span>
<span data-smark='{"action":"position"}'>#</span>
<input name="name" placeholder="Group name" type="text" data-smark>
<button data-smark='{"action":"removeItem","hotkey":"-"}' title="Remove group">β</button>
</summary>
<div class="sg-body">
<label class="sg-tutor">Tutor: <input name="tutor" placeholder="Tutor name" type="text" data-smark></label>
<div class="sg-students">
<strong>Students</strong>
<ul data-smark='{"type":"list","name":"students","sortable":true,"movingDepth":2,"min_items":0}'>
<li data-smark='{"role":"empty_list"}' class="sg-empty">No students yet</li>
<li>
<details>
<summary>
<span data-smark='label' title="Drag to reorder or move between groups" class="sg-handle">β Ώ</span>
<input name="name" placeholder="Student name" type="text" data-smark>
<button data-smark='{"action":"removeItem","hotkey":"-"}' title="Remove student">β</button>
</summary>
<div data-smark='{"type":"form","name":"grades"}' class="sg-grades">
<div class="sg-grade-col">
<div class="sg-grade-label">Math</div>
<div data-smark='{"type":"list","name":"math","min_items":0}'>
<div data-smark='{"role":"empty_list"}' class="sg-empty">β
</div>
<input type="number" step="0.1" min="0" max="10" data-smark>
</div>
<div class="sg-grade-btns">
<button data-smark='{"action":"addItem","hotkey":"+","context":"math"}' title="Add grade">β</button>
<button data-smark='{"action":"removeItem","hotkey":"-","context":"math"}' title="Remove grade">β</button>
</div>
</div>
<div class="sg-grade-col">
<div class="sg-grade-label">Literature</div>
<div data-smark='{"type":"list","name":"literature","min_items":0}'>
<div data-smark='{"role":"empty_list"}' class="sg-empty">β
</div>
<input type="number" step="0.1" min="0" max="10" data-smark>
</div>
<div class="sg-grade-btns">
<button data-smark='{"action":"addItem","hotkey":"+","context":"literature"}' title="Add grade">β</button>
<button data-smark='{"action":"removeItem","hotkey":"-","context":"literature"}' title="Remove grade">β</button>
</div>
</div>
<div class="sg-grade-col">
<div class="sg-grade-label">Science</div>
<div data-smark='{"type":"list","name":"science","min_items":0}'>
<div data-smark='{"role":"empty_list"}' class="sg-empty">β
</div>
<input type="number" step="0.1" min="0" max="10" data-smark>
</div>
<div class="sg-grade-btns">
<button data-smark='{"action":"addItem","hotkey":"+","context":"science"}' title="Add grade">β</button>
<button data-smark='{"action":"removeItem","hotkey":"-","context":"science"}' title="Remove grade">β</button>
</div>
</div>
</div>
</details>
</li>
</ul>
<button data-smark='{"action":"addItem","hotkey":"+","context":"students"}' title="Add student">β Add Student</button>
</div>
</div>
</details>
</div>
</li>
</ul>
<button data-smark='{"action":"addItem","hotkey":"+","context":"groups"}' title="Add group">β Add Group</button>
</div>
</div>
#myForm .sg { max-width: 520px; font-size: 0.95em; font-family: sans-serif; }
#myForm .sg ul { list-style: none; padding: 0; margin: 0; }
#myForm .sg-header {
display: flex; gap: 0.5em; margin-bottom: 0.6em; flex-wrap: wrap;
}
#myForm .sg-header input {
flex: 1; min-width: 120px; padding: 0.35em 0.5em;
border: 1px solid #ccc; border-radius: 4px;
}
#myForm .sg-card {
border: 1px solid #ddd; border-radius: 8px;
margin: 0.4em 0; background: #fafafa;
overflow: hidden;
}
#myForm .sg-card details[open] { padding-bottom: 0.4em; }
#myForm .sg-card summary {
display: flex; align-items: center; gap: 0.4em;
padding: 0.35em 0.5em; user-select: none;
background: #fff; border-radius: 8px;
}
#myForm .sg-card details[open] summary { border-radius: 8px 8px 0 0; border-bottom: 1px solid #eee; }
#myForm .sg-handle { cursor: grab; color: #aaa; user-select: none; }
#myForm .sg-card summary input[type="text"] {
flex: 1; padding: 0.25em 0.4em;
border: 1px solid #ccc; border-radius: 4px;
}
#myForm .sg-body { padding: 0.4em 0.5em 0.2em 1.8em; }
#myForm .sg-tutor { display: flex; align-items: center; gap: 0.4em; margin-bottom: 0.3em; font-size: 0.9em; }
#myForm .sg-tutor input { flex: 1; padding: 0.2em 0.4em; border: 1px solid #ccc; border-radius: 4px; }
#myForm .sg-students { margin-top: 0.3em; }
#myForm .sg-students > strong { font-size: 0.85em; color: #555; display: block; margin-bottom: 0.2em; }
#myForm .sg-students ul {
max-height: 240px; overflow-y: auto;
border: 1px solid #eee; border-radius: 4px; padding: 0.2em 0.3em;
background: #fff;
}
#myForm .sg-students ul li { margin: 0.15em 0; }
#myForm .sg-students ul details summary {
display: flex; align-items: center; gap: 0.3em; padding: 0.2em 0.3em;
border-radius: 4px; background: #fafafa; border: 1px solid #eee;
}
#myForm .sg-students ul details[open] summary { border-radius: 4px 4px 0 0; border-bottom: 0; }
#myForm .sg-students ul details[open] { background: #fafafa; }
#myForm .sg-students ul li input[type="text"] {
flex: 1; padding: 0.2em 0.4em;
border: 1px solid #ccc; border-radius: 4px; font-size: 0.9em;
}
#myForm .sg-grades {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px;
padding: 0.3em 0.3em 0.2em 0.3em;
}
#myForm .sg-grade-col {
display: flex; flex-direction: column; gap: 2px;
}
#myForm .sg-grade-label {
font-weight: bold; text-align: center; font-size: 0.85em;
padding: 0.15em 0; border-bottom: 1px solid #ddd;
}
#myForm .sg-grade-col input[type="number"] {
width: 100%; box-sizing: border-box;
padding: 0.15em 0.3em;
border: 1px solid #ccc; border-radius: 4px;
}
#myForm .sg-grade-btns {
display: flex; gap: 2px; justify-content: center; margin-top: 2px;
}
#myForm .sg-grade-btns button {
padding: 0.1em 0.4em; border: 1px solid #ccc; border-radius: 4px;
background: #fff; cursor: pointer; font-size: 0.85em; line-height: 1.4;
}
#myForm .sg-empty { font-style: italic; color: #aaa; padding: 0.5em; font-size: 0.9em; }
#myForm .sg button {
padding: 0.2em 0.6em; border: 1px solid #ccc; border-radius: 4px;
background: #fff; cursor: pointer; line-height: 1.5;
}
#myForm .sg button:disabled { opacity: 0.4; }
#myForm .sg [data-hotkey] { position: relative; }
#myForm .sg [data-hotkey]::after {
content: "Ctrl+" attr(data-hotkey);
position: absolute; top: -1.6em; left: 0;
font-size: 0.7em; background: #333; color: #fff;
padding: 1px 4px; border-radius: 3px; white-space: nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"name": "Springfield Elementary",
"level": "Primary",
"year": "2025/2026",
"groups": [
{
"name": "Class A",
"tutor": "Mr. Smith",
"students": [
{"name": "Lisa Simpson", "grades": {"math": [9.5, 8.0], "literature": [7.3], "science": [8.8]}},
{"name": "Bart Simpson", "grades": {"math": [4.0], "literature": [5.5], "science": [3.0]}}
]
},
{
"name": "Class B",
"tutor": "Ms. Johnson",
"students": [
{"name": "Milhouse Van Houten", "grades": {"math": [6.0], "literature": [7.0], "science": [6.5]}}
]
}
]
}
});
π Nested hierarchy. Three levels of nesting: groups β students β grades produce deeply structured JSON automatically.
π Card layout. Each group is a card with a scrollable student list (max-height, overflow-y: auto) β keeps the page tidy even with many students.
π Cross-list drag. movingDepth: 2 on the students list lets users drag students between groups (sibling distance = 2: student β group β siblingβs students).
π Collapsible sections. Both groups and students use <details>/<summary> to keep the view compact. A group shows its name in the summary; opening it reveals the tutor and student list. Likewise for each student.
π Nested <details>. SmarkForm handles keyboard navigation correctly even with nested collapsible sections β Shift+Space toggles, Enter navigates, and auto-open works as expected.
π Auto-disable. Triggers disable themselves at their listβs min_items/max_items boundary β no code needed.
π position action. Span with data-smark='{"action":"position"}' auto-numbers each group.
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
Deeply nested forms
Despite of usability concerns, there is no limit in form nesting depth.
In fact, all examples in this chapter are entirely built with SmarkForm itself with no additional JS code.
π Including the JSON playground editor and the β¬οΈ Export, β¬οΈ Import, β»οΈ Reset and β Clear buttons are just SmarkForm trigger components that work out of the box.
π€ β¦itβs just that part is omitted in the shown HTML source to keep the examples simple and focused on the subject they are intended to illustrate.
The editor scaffold (Export / Import / Reset / Clear buttons + the JSON textarea) is injected externally by the documentation framework. It is not part of the example HTML β so what you see in the HTML tab is exactly the code you would write yourself.
π΅οΈ If you go to any of the interactive examples in this page (or in the rest of the documentation) and check the π Edit checkbox, youβll be editing the real example source code. Check the π Include playground editor checkbox to also show the editor scaffold in the preview (it is injected externally and is not part of the example HTML).
-
If you look close to the HTML source, you will see that
β¬οΈ Importandβ¬οΈ Exportbuttons 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,β¬οΈ Importandβ Clearbuttons 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 theCtrlkey 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.
<div id="myForm">
<p>
<button data-smark='{"action":"removeItem","hotkey":"-","context":"schedule"}' title='Less intervals'>β</button>
<button data-smark='{"action":"addItem","hotkey":"+","context":"schedule"}' title='More intrevals'>β</button>
<strong data-smark="label">Schedule:</strong>
<span data-smark='{"type":"list","name":"schedule","min_items":0,"max_items":3,"exportEmpties":true}'>
<span>
<input class='small' data-smark type='time' name='start'> to <input class='small' data-smark type='time' name='end'>
</span>
<span data-smark='{"role":"empty_list"}'>(Closed)</span>
<span data-smark='{"role":"separator"}'>, </span>
<span data-smark='{"role":"last_separator"}'> and </span>
</span>
</p>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"schedule": [
{
"start": "09:00:00",
"end": "13:00:00"
},
{
"start": "14:00:00",
"end": "18:00:00"
}
]
}
});
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
β¦This is fine for a simple case, and leaves the door open for easily increasing the number of intervals allowed in the schedule.
But it could look kind of messy if you need to introduce several schedules that may have different number of intervals.
Letβs imagine a hotel wanting to manage the scheduling of all the services it offersβ¦
π Weβll see how this gets implemented in the Mixins section below, where the #scheduleRow template makes the pattern clean and reusable.
Mixins
The hotel scheduling example below uses a single mixin template (#scheduleRow) instead of repeating the same .schedule-row markup four times. With Mixin Types, you define that pattern once inside a <template> element and reference it from as many usage sites as you need.
Each usage site keeps its own identity:
nameis supplied by the placeholder, not the template β every row gets its own field name and data path.data-forslots β e.g.<span data-for="label">replaces the<span id="label">inside the template.- Option overrides β any
data-smarkin the placeholder overrides the template default (e.g."max_items":5on a specific row).
The <template> tag also accepts optional siblings:
<style>β injected into<head>exactly once, no matter how many times the mixin is used.<script>β a per-instance hook (discussed later).
Press and hold
Ctrlto reveal the available hotkeys on the corresponding β / β buttons depending on where the focus is.
<div id="myForm">
<div class="schtbl" data-smark='{"type":"form","name":"schedules"}'>
<div data-smark='{"type":"#scheduleRow","name":"rcpt_schedule"}'>
<span data-for="label">ποΈ Reception:</span></div>
<div data-smark='{"type":"#scheduleRow","name":"bar_schedule"}'>
<span data-for="label">πΈ Bar:</span></div>
<div data-smark='{"type":"#scheduleRow","name":"restaurant_schedule"}'>
<span data-for="label">π½οΈ Restaurant:</span></div>
<div data-smark='{"type":"#scheduleRow","name":"pool_schedule"}'>
<span data-for="label">π Pool:</span></div>
</div>
</div>
<template id="scheduleRow">
<div class="schedule-row"
data-smark='{"type":"list","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
<strong data-smark='{"role":"header"}'><span id="label">Schedule</span></strong>
<span class='time_slot' data-smark='{"role":"empty_list"}'>(Closed)</span>
<span class='time_slot'>
<span class='time_from'>From <input class='small' data-smark type='time' name='start'></span>
<span class='time_to'>to <input class='small' data-smark type='time' name='end'></span>
</span>
<span data-smark='{"role":"footer"}'>
<button data-smark='{"action":"removeItem","hotkey":"-"}' title="Less intervals">β</button>
<button data-smark='{"action":"addItem","hotkey":"+"}' title="More intervals">β</button>
</span>
</div>
<style>
.schtbl {
display: flex; flex-direction: column; gap: 0.1em;
}
.schedule-row {
display: grid;
grid-template-columns: 10em 1fr auto;
align-items: start;
gap: 0.25em 0.5em;
padding: 0.2em 0.4em;
border-radius: 0.3em;
}
.schedule-row:nth-child(even) {
background-color: rgba(128, 128, 128, 0.08);
}
.schedule-row > [data-role="header"] {
grid-column: 1; grid-row: 1;
padding-top: 0.3em;
}
.schedule-row > .time_slot { grid-column: 2; }
.schedule-row > [data-role="empty_list"] { padding-right: 5em; }
.schedule-row > [data-role="footer"] {
grid-column: 3; grid-row: 1 / -1; align-self: center; white-space: nowrap;
}
.time_slot {
display: flex; flex-wrap: wrap; gap: .15em .4em; align-items: center;
justify-content: flex-end;
}
.time_slot input.small { max-width: 5.5em; }
.time_from, .time_to { display: flex; align-items: center; gap: .2em; white-space: nowrap; }
@media (max-width: 30em) {
.schedule-row { grid-template-columns: 1fr auto; }
.schedule-row > .time_slot,
.schedule-row > [data-role="empty_list"] {
grid-column: 1; padding-left: 0.5em; text-align: right;
}
.schedule-row > [data-role="footer"] { grid-column: 2; grid-row: 2 / -1; }
}
</style>
</template>
#myForm .schtbl {
display: flex;
flex-direction: column;
gap: 0.1em;
}
#myForm .schedule-row {
display: grid;
grid-template-columns: 10em 1fr auto;
align-items: start;
gap: 0.25em 0.5em;
padding: 0.2em 0.4em;
border-radius: 0.3em;
}
#myForm .schedule-row:nth-child(even) {
background-color: rgba(128, 128, 128, 0.08);
}
#myForm .schedule-row > [data-role="header"] {
grid-column: 1;
grid-row: 1;
padding-top: 0.3em;
}
#myForm .schedule-row > .time_slot {
grid-column: 2;
}
#myForm .schedule-row > [data-role="empty_list"] {
padding-right: 5em;
}
#myForm .schedule-row > [data-role="footer"] {
grid-column: 3;
grid-row: 1 / -1;
align-self: center;
white-space: nowrap;
}
#myForm .time_slot {
display: flex;
flex-wrap: wrap;
gap: 0.15em 0.4em;
align-items: center;
justify-content: flex-end;
}
#myForm .time_slot input.small{
max-width: 5.5em;
}
#myForm .time_from,
#myForm .time_to {
display: flex;
align-items: center;
gap: 0.2em;
white-space: nowrap;
}
#myForm .period-dates {
display: flex;
flex-wrap: wrap;
gap: 0.25em 1.5em;
align-items: baseline;
margin: 0.3em 0;
justify-content: flex-end;
}
#myForm .period-date {
white-space: nowrap;
}
@media (max-width: 30em) {
#myForm .schedule-row {
grid-template-columns: 1fr auto;
}
#myForm .schedule-row > [data-role="header"] {
grid-column: 1;
grid-row: 1;
padding-top: 0;
}
#myForm .schedule-row > .time_slot,
#myForm .schedule-row > [data-role="empty_list"] {
grid-column: 1;
padding-left: 0.5em;
text-align: right;
}
#myForm .schedule-row > [data-role="footer"] {
grid-column: 2;
grid-row: 2 / -1;
}
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"schedules": {
"rcpt_schedule": [
{
"start": "00:00:00",
"end": "23:59:00"
}
],
"bar_schedule": [
{
"start": "11:00:00",
"end": "23:00:00"
}
],
"restaurant_schedule": [
{
"start": "07:30:00",
"end": "10:30:00"
},
{
"start": "13:00:00",
"end": "15:30:00"
},
{
"start": "19:00:00",
"end": "22:00:00"
}
],
"pool_schedule": [
{
"start": "09:00:00",
"end": "20:00:00"
}
]
}
}
});
π Here we replaced the original <table> layout with CSS grid to prevent horizontal scrollbars when multiple intervals are added:
- Each schedule list (
.schedule-row) is a CSS grid with three columns:10em label | 1fr slots | auto controls. - Additional intervals stack vertically in the middle column instead of widening the row.
- The footer role holds the β/β buttons, which span all slot rows via
grid-row: 1 / -1so 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:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
Want to learn more about mixins? See the full reference in Mixin Types.
Nested lists and forms
Great! Now we have all the scheduling information of or hotel services.
β¦or maybe not:
Some services may have different schedules for different days of the week or depending on the season (think in the swimming pool in winterβ¦).
Since we can make lists of forms, we can also nest more forms and lists inside every list item and so forth to any depth.
π Letβs focus on the seasons by now:
<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> <input data-smark type='date' name='start_date'></span>
<span class='period-date'><label data-smark>End Date:</label> <input data-smark type='date' name='end_date'></span>
</p>
<div class="schtbl" data-smark='{"type":"form","name":"schedules"}'>
<div data-smark='{"type":"#scheduleRow","name":"rcpt_schedule"}'>
<span data-for="label">ποΈ Reception:</span></div>
<div data-smark='{"type":"#scheduleRow","name":"bar_schedule"}'>
<span data-for="label">πΈ Bar:</span></div>
<div data-smark='{"type":"#scheduleRow","name":"restaurant_schedule"}'>
<span data-for="label">π½οΈ Restaurant:</span></div>
<div data-smark='{"type":"#scheduleRow","name":"pool_schedule"}'>
<span data-for="label">π Pool:</span></div>
</div>
</fieldset>
<script>
const item = this;
item.parent.on('AfterAction_addItem', async function(ev) {
if (!ev.data || ev.data.getPath() !== item.getPath()) return;
const idx = parseInt(item.getPath().split('/').pop());
const items = await item.parent.export({ exportEmpties: true });
const prev = idx > 0 ? items[idx - 1] : null;
const next = idx < items.length - 1 ? items[idx + 1] : null;
const fmtDate = d =>
d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
let startDate = null;
if (prev?.end_date) {
const d = new Date(prev.end_date + 'T00:00:00');
d.setDate(d.getDate() + 1);
startDate = fmtDate(d);
} else if (!prev) {
startDate = fmtDate(new Date());
}
let endDate = null;
if (startDate && !next) {
if (prev?.start_date && prev?.end_date) {
const pStart = new Date(prev.start_date + 'T00:00:00');
const pEnd = new Date(prev.end_date + 'T00:00:00');
const nStart = new Date(startDate + 'T00:00:00');
const pStartsFirst = pStart.getDate() === 1;
const pEndsLast = pEnd.getDate() ===
new Date(pEnd.getFullYear(), pEnd.getMonth() + 1, 0).getDate();
if (pStartsFirst && pEndsLast) {
const months = (pEnd.getFullYear() - pStart.getFullYear()) * 12 +
(pEnd.getMonth() - pStart.getMonth()) + 1;
endDate = fmtDate(
new Date(nStart.getFullYear(), nStart.getMonth() + months, 0));
} else {
const days = Math.round((pEnd - pStart) / 86400000);
const d = new Date(nStart);
d.setDate(d.getDate() + days);
endDate = fmtDate(d);
}
}
} else if (startDate && next?.start_date) {
if (next.start_date > startDate) {
const d = new Date(next.start_date + 'T00:00:00');
d.setDate(d.getDate() - 1);
endDate = fmtDate(d);
}
}
if (endDate && startDate && endDate < startDate) endDate = null;
const startField = item.find('start_date');
const endField = item.find('end_date');
if (startField) await startField.import(startDate, { setDefault: false });
if (endField) await endField.import(endDate, { setDefault: false });
});
</script>
</template>
<template id="scheduleRow">
<div class="schedule-row"
data-smark='{"type":"list","min_items":0,"max_items":3,"exportEmpties":false,"value":[{}]}'>
<strong data-smark='{"role":"header"}'><span id="label">Schedule</span></strong>
<span class='time_slot' data-smark='{"role":"empty_list"}'>(Closed)</span>
<span class='time_slot'>
<span class='time_from'>From <input class='small' data-smark type='time' name='start'></span>
<span class='time_to'>to <input class='small' data-smark type='time' name='end'></span>
</span>
<span data-smark='{"role":"footer"}'>
<button data-smark='{"action":"removeItem","hotkey":"-"}' title="Less intervals">β</button>
<button data-smark='{"action":"addItem","hotkey":"+"}' title="More intervals">β</button>
</span>
</div>
<style>
.schtbl {
display: flex; flex-direction: column; gap: 0.1em;
}
.schedule-row {
display: grid;
grid-template-columns: 10em 1fr auto;
align-items: start;
gap: 0.25em 0.5em;
padding: 0.2em 0.4em;
border-radius: 0.3em;
}
.schedule-row:nth-child(even) {
background-color: rgba(128, 128, 128, 0.08);
}
.schedule-row > [data-role="header"] {
grid-column: 1; grid-row: 1;
padding-top: 0.3em;
}
.schedule-row > .time_slot { grid-column: 2; }
.schedule-row > [data-role="empty_list"] { padding-right: 5em; }
.schedule-row > [data-role="footer"] {
grid-column: 3; grid-row: 1 / -1; align-self: center; white-space: nowrap;
}
.time_slot {
display: flex; flex-wrap: wrap; gap: .15em .4em; align-items: center;
justify-content: flex-end;
}
.time_slot input.small { max-width: 5.5em; }
.time_from, .time_to { display: flex; align-items: center; gap: .2em; white-space: nowrap; }
@media (max-width: 30em) {
.schedule-row { grid-template-columns: 1fr auto; }
.schedule-row > .time_slot,
.schedule-row > [data-role="empty_list"] {
grid-column: 1; padding-left: 0.5em; text-align: right;
}
.schedule-row > [data-role="footer"] { grid-column: 2; grid-row: 2 / -1; }
}
</style>
</template>
#myForm .schtbl {
display: flex;
flex-direction: column;
gap: 0.1em;
}
#myForm .schedule-row {
display: grid;
grid-template-columns: 10em 1fr auto;
align-items: start;
gap: 0.25em 0.5em;
padding: 0.2em 0.4em;
border-radius: 0.3em;
}
#myForm .schedule-row:nth-child(even) {
background-color: rgba(128, 128, 128, 0.08);
}
#myForm .schedule-row > [data-role="header"] {
grid-column: 1;
grid-row: 1;
padding-top: 0.3em;
}
#myForm .schedule-row > .time_slot {
grid-column: 2;
}
#myForm .schedule-row > [data-role="empty_list"] {
padding-right: 5em;
}
#myForm .schedule-row > [data-role="footer"] {
grid-column: 3;
grid-row: 1 / -1;
align-self: center;
white-space: nowrap;
}
#myForm .time_slot {
display: flex;
flex-wrap: wrap;
gap: 0.15em 0.4em;
align-items: center;
justify-content: flex-end;
}
#myForm .time_slot input.small{
max-width: 5.5em;
}
#myForm .time_from,
#myForm .time_to {
display: flex;
align-items: center;
gap: 0.2em;
white-space: nowrap;
}
#myForm .period-dates {
display: flex;
flex-wrap: wrap;
gap: 0.25em 1.5em;
align-items: baseline;
margin: 0.3em 0;
justify-content: flex-end;
}
#myForm .period-date {
white-space: nowrap;
}
@media (max-width: 30em) {
#myForm .schedule-row {
grid-template-columns: 1fr auto;
}
#myForm .schedule-row > [data-role="header"] {
grid-column: 1;
grid-row: 1;
padding-top: 0;
}
#myForm .schedule-row > .time_slot,
#myForm .schedule-row > [data-role="empty_list"] {
grid-column: 1;
padding-left: 0.5em;
text-align: right;
}
#myForm .schedule-row > [data-role="footer"] {
grid-column: 2;
grid-row: 2 / -1;
}
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {"allowLocalMixinScripts":"allow",
"value": {
"periods": [
{
"start_date": "2025-01-01",
"end_date": "2025-06-30",
"schedules": {
"rcpt_schedule": [
{"start": "00:00:00", "end": "23:59:00"}
],
"bar_schedule": [
{"start": "10:00:00", "end": "23:30:00"}
],
"restaurant_schedule": [
{"start": "07:30:00", "end": "10:30:00"},
{"start": "13:00:00", "end": "15:30:00"},
{"start": "19:00:00", "end": "22:00:00"}
],
"pool_schedule": []
}
},
{
"start_date": "2025-07-01",
"end_date": "2025-12-31",
"schedules": {
"rcpt_schedule": [
{"start": "00:00:00", "end": "23:59:00"}
],
"bar_schedule": [
{"start": "10:00:00", "end": "23:30:00"}
],
"restaurant_schedule": [
{"start": "07:30:00", "end": "10:30:00"},
{"start": "13:00:00", "end": "15:30:00"},
{"start": "19:00:00", "end": "22:00:00"}
],
"pool_schedule": [
{"start": "09:30:00", "end": "19:30:00"}
]
}
}
]
}
});
π Two templates, one example:
#scheduleRow(inner) β the time-interval list template.#periodItem(outer) β wraps the whole period fieldset and uses#scheduleRowinside it, demonstrating mixin composition.
π Smart date prefill (the <script> in #periodItem):
- Registers an
AfterAction_addItemlistener that fires after thesource:".-1"import, so it always operates on the final data. - Computes
start_dateandend_datebased on siblings:
| Situation | start_date | end_date |
|---|---|---|
| No previous period | today | blank |
| Has previous (no end_date) | blank | blank |
| Has previous, no next | prev.end + 1 day | start + same duration as prev |
| Inserted β gap exists | prev.end + 1 day | next.start β 1 day |
| Inserted β contiguous | prev.end + 1 day | blank (user decides) |
end_dateis cleared whenever it would predatestart_date.
π Empty-list message. Remove all periods to see π Out of Service.
π Sortable periods. Drag and drop to reorder.
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
β‘ There is no theoretical limit to the depth of nesting beyond the logical usability concerns.
<script>hooks inside mixin templates are a powerful way to add component-specific behavior.Within them,
thisrefers to the individual mixin instance, allowing you to listen to events and manipulate data at the item level, without affecting other instances of the same mixin.The former example uses this feature to implement smart date prefill logic that considers the position of the new period and its siblings to suggest appropriate start and end dates, streamlining the user experience when adding new periods.
π Letβs try it!
Item duplication and closure state
Adding similar items to a list of complex and configurable subforms βlike the periods list in our exampleβ can be tedious if users have to re-enter all fields each time.
The previous example already includes a duplicate (β¨) button that uses source:".-1" to prefill a new item with data copied from the previous one. It also sets min_items:0 with value:[{}] so the list starts with one empty item (good UX) yet can be emptied entirely, at which point the π Out of Service message (the empty_list role) is shown.
These techniques work together:
source:".-1"on anaddItemtrigger β the new item receives the previous itemβs data immediately after render.min_items:0β allows the list to be fully emptied.value:[{}]β ensures one empty item appears by default even withmin_items:0, so users are never greeted with a blank list.empty_listrole β provides feedback when the list has no items.
A note on empty values
Take a look to the HTML source of the previous example and pay attention to where and how the exportEntries property is used in the lists:
- For the periods list we set exportEmpties to true, overidding its default value (false).
- This way, if a period is added (intentional), it gets exported even if not filled.
- This is because the user may be saving his work to continue later or just mean there is a period but we donβt know its data yet.
- For the schedules lists we set exportEmpties to false (necessary to prevent inheriting the true value we just set). This way:
- When a period is added, all schedules are layed out with their default value (one empty time interval ready to be filled).
- If the user leaves any unfilled (because of being inappropriate) and neglects removing it, it will be just swallowed when exporting the form data.
- This way, when importing the exported data (or if item is duplicated with the
β¨button), the unfilled intervals are correctly shown as β(Closed)β.
Nesting Mixins
The nested periods example already combines two mixins: #periodItem (the outer one, wrapping the period fieldset) references #scheduleRow (the inner one, providing the time-interval grid). The <style> from #scheduleRow is injected once into <head> regardless of how many times either mixin is used β mixin styles are shared, not duplicated.
Looking at that exampleβs HTML source you can see both templates defined side by side: #scheduleRow is the inner list template; #periodItem is the outer component that uses it. Extract any repeated piece of UI into a similar template and reference it with type: "#yourTemplateName".
Import and Export Data
Exporting and importing data in SmarkForm cannot be easier.
The β¬οΈ Export, β¬οΈ Import and β Clear buttons used in all examples in this documentation are just triggers that call the export and import actions on the whole form (their context):
β¬οΈ Exportexports the whole form to the βeditorβ textarea (its target).β¬οΈ Importimports the JSON data from the βeditorβ textarea into the form (its target).β Clearclears 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
π Editon 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.
<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
ποΈ HTMLtab and check the π checkbox. - Edit the email in the
actionattribute of the<form>element. - Click the
βΆοΈ Runbutton to reload theποΈ Previewtab 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 withmailto:actions. Use the default (URL-encoded) encoding formailto:.
You can also intercept or extend the submission via SmarkForm events:
BeforeAction_submit(fired before sending β you canpreventDefault()to cancel) andAfterAction_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,β¬οΈ Importandβ Clearbuttons 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:
<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:
- Keep the min_items to its default value of 1, so that the list cannot be empty.
- Add a little CSS to make the disabled buttons more evident.
<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:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto 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.
<div id="myForm">
<button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>π§Ή</button>
<button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>β</button>
<button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>β </button>
<strong data-smark="label">Phones:</strong>
<ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
<li class="row">
<label data-smark>π Telephone
<span data-smark='{"action":"position"}'>N</span>
</label>
<button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>β</button>
<input type="tel" data-smark>
<button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>β </button>
</li>
</ul>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"phones": [
"+1 555 867 5309",
"+1 555 234 5678"
]
}
});
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto 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-hotkeyattribute 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.
<div id="myForm">
<p>
<label data-smark='{"type": "label"}'>Name:</label>
<input name='name' data-smark='{"type": "input"}' />
</p>
<p>
<label data-smark='{"type": "label"}'>Surname:</label>
<input name='surname' data-smark='{"type": "input"}' />
</p>
<button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>π§Ή</button>
<button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>β</button>
<button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>β </button>
<strong data-smark="label">Phones:</strong>
<ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
<li class="row">
<label data-smark>π Telephone
<span data-smark='{"action":"position"}'>N</span>
</label>
<button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>β</button>
<input type="tel" data-smark>
<button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>β </button>
</li>
</ul>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"name": "John",
"surname": "Doe",
"phones": [
"+1 555 867 5309",
"+1 555 234 5678"
]
}
});
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
Collapsible sections
HTMLβs native <details> and <summary> elements provide a simple, accessible way to create collapsible content β no JavaScript or special SmarkForm properties needed. SmarkForm fields placed inside a <details> element work exactly as they would anywhere else; the browser handles the show/hide toggle natively.
A particularly useful pattern is to use <details> elements as list items, placing an identifying field inside the <summary>. This way, even when an item is collapsed, its key information remains visible so the user can quickly scan the list.
The following example shows a contact list where each item is a <details> element. The <summary> contains the contactβs name so it is always visible, while the full details (email and phone) are revealed on expansion:
<div id="myForm">
<div data-smark='{"name":"contacts","type":"list","min_items":1,"exportEmpties":false}'>
<details>
<summary>
<input data-smark type="text" name="fullname" placeholder="Full name">
</summary>
<div class="contact-details">
<input data-smark type="email" name="email" placeholder="Email">
<input data-smark type="tel" name="phone" placeholder="Phone">
<button data-smark='{"action":"removeItem","failback":"clear","hotkey":"Delete"}' title="Remove">π</button>
</div>
</details>
</div>
<button data-smark='{"action":"addItem","context":"contacts","hotkey":"+"}'>β Add contact</button>
<textarea data-smark name="notes" placeholder="Notes about this contact list"></textarea>
</div>
#myForm {
font-family: sans-serif;
}
#myForm details {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 6px;
padding: 4px 8px;
}
#myForm summary {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
list-style: none;
user-select: none;
}
#myForm summary::before {
content: "βΆ";
font-size: .75em;
transition: transform .15s;
flex-shrink: 0;
}
#myForm details[open] > summary::before {
transform: rotate(90deg);
}
#myForm summary input {
flex: 1;
min-width: 0;
font-weight: bold;
border: none;
background: transparent;
outline: none;
}
#myForm .contact-details {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 6px 0 2px 1.2em;
}
#myForm .contact-details input {
flex: 1;
min-width: 120px;
}
#myForm .contact-details button {
margin-left: auto;
}
#myForm textarea[name="notes"] {
display: block;
width: 100%;
min-height: 60px;
margin-top: 8px;
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: inherit;
box-sizing: border-box;
resize: vertical;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"contacts": [
{"fullname":"Alice Smith","email":"alice@example.com","phone":"+1 555 100 0001"},
{"fullname":"Bob Jones","email":"bob@example.com","phone":"+1 555 100 0002"},
{"fullname":"Carol White","email":"carol@example.com","phone":"+1 555 100 0003"}
],
"notes": ""
}
});
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto clear the whole form.
π‘ The JSON playground editor is part of the SmarkForm form itself β it is just omitted from the code snippets to keep the examples focused on what matters.
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
π Notice that clicking a contactβs name row (or the arrow at the left) toggles it. The name input inside <summary> is always visible and editable regardless of the collapsed/expanded state.
The same technique works for any form section, not just list items. Wrap a
<div data-smark>or<fieldset data-smark>in a<details>element and add a<summary>heading β no SmarkForm-specific options needed. Theopenattribute on<details>controls the initial state: include it to start expanded (the default), or omit it to start collapsed.
Smooth navigation
As you may have already noticed in the preceding examples, SmarkForm provides an intuitive interface to facilitate users effortlessly discover how to fluently fill all the data in the form without bothering with the interface.
π Notice you can navigate smoothly between form fields by typing Enter (forward) and Shift+Enter (backward).
So, when you finish filling a field, you can just press Enter to move to the next one.
This is not only more convenient than Tab and Shift+Tab. More than that: it skips controls providing a more fluid experience when you are just filling data in.
In case of a textarea, use
Ctrl+Enterinstead, sinceEnteralone 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
Ctrlkey 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:
<div id="myForm">
<div data-smark='{"type": "list", "name": "phonelist", "sortable": true}'>
<fieldset>
<legend>
<span data-smark='{"action":"removeItem", "hotkey":"-"}' title='Delete this phonebook entry' style='cursor:pointer'>[β]</span>
<strong>
Contact
<span data-smark='{"action":"position"}'>N</span>
</strong>
</legend> <p>
<label data-smark='{"type": "label"}'>Name:</label>
<input name='name' data-smark='{"type": "input"}' />
</p>
<p>
<label data-smark='{"type": "label"}'>Surname:</label>
<input name='surname' data-smark='{"type": "input"}' />
</p>
<button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>π§Ή</button>
<button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>β</button>
<button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>β </button>
<strong data-smark="label">Phones:</strong>
<ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
<li class="row">
<label data-smark>π Telephone
<span data-smark='{"action":"position"}'>N</span>
</label>
<button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>β</button>
<input type="tel" data-smark>
<button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>β </button>
</li>
</ul> </fieldset>
</div>
<p style="text-align: right; margin-top: 1em">
<b>Total entries:</b>
<span data-smark='{"action":"count", "context": "phonelist"}'>M</span>
</p>
<button
data-smark='{"action":"addItem","context":"phonelist","hotkey":"+"}'
style="float: right; margin-top: 1em"
>β Add Contact</button>
</div>
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"));
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
β¨ In the Preview tab, a JSON playground editor is available with handy buttons:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto 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, likedisplay: none;, since it will also prevent the::beforeor::afterpseudo-element from appearing too.Better use
visibility: hidden;oropacity: 0;to hide the button andwidth: 0px;and/orheight: 0px;as needed to prevent them from taking space in the layout.
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.
-
afterRenderfires 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. -
beforeUnrenderfires before an item is removed from the DOM. Remove the βvisibleβ class and return aPromisethat 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.
<div id="myForm">
<div data-smark='{"type": "list", "name": "phonelist", "sortable": true}'>
<fieldset>
<legend>
<span data-smark='{"action":"removeItem", "hotkey":"-"}' title='Delete this phonebook entry' style='cursor:pointer'>[β]</span>
<strong>
Contact
<span data-smark='{"action":"position"}'>N</span>
</strong>
</legend> <p>
<label data-smark='{"type": "label"}'>Name:</label>
<input name='name' data-smark='{"type": "input"}' />
</p>
<p>
<label data-smark='{"type": "label"}'>Surname:</label>
<input name='surname' data-smark='{"type": "input"}' />
</p>
<button data-smark='{"action":"removeItem", "context":"phones", "target":"*", "hotkey":"Delete", "preserve_non_empty":true}' title='Remove unused fields'>π§Ή</button>
<button data-smark='{"action":"removeItem", "context":"phones", "hotkey":"-", "preserve_non_empty":true}' title='Remove phone number'>β</button>
<button data-smark='{"action":"addItem","context":"phones", "hotkey":"+"}' title='Add phone number'>β </button>
<strong data-smark="label">Phones:</strong>
<ul data-smark='{"name": "phones", "of": "input", "sortable":true, "max_items":5}'>
<li class="row">
<label data-smark>π Telephone
<span data-smark='{"action":"position"}'>N</span>
</label>
<button data-smark='{"action":"removeItem", "hotkey":"-"}' title='Remove this phone number'>β</button>
<input type="tel" data-smark>
<button data-smark='{"action":"addItem", "hotkey":"+"}' title='Insert phone number'>β </button>
</li>
</ul> </fieldset>
</div>
<p style="text-align: right; margin-top: 1em">
<b>Total entries:</b>
<span data-smark='{"action":"count", "context": "phonelist"}'>M</span>
</p>
<button
data-smark='{"action":"addItem","context":"phonelist","hotkey":"+"}'
style="float: right; margin-top: 1em"
>β Add Contact</button>
</div>
.animated_item {
transform: translateX(-100%); /* Start off-screen to the left */
opacity: 0; /* Optional: Start invisible for smoother effect */
/* Transition for removal effect */
transition:
transform 200ms ease-out,
opacity 200ms ease-out;
}
.animated_item.ongoing {
transform: translateX(0); /* End at original position */
opacity: 1; /* Optional: Fully visible */
transition:
transform 200ms ease-in,
opacity 200ms ease-in;
}
#myForm li.row button[data-smark] {
visibility: hidden;
width: 0px;
pointer-events: none;
}
#myForm li.row button[data-smark]::before {
visibility: visible;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
/* Hide list bullets */
#myForm ul li {
list-style-type: none;
}
/* Make disabled buttons more evident: */
#myForm :disabled {
opacity: 0.4;
}
const myForm = new SmarkForm(document.getElementById("myForm"));const delay = ms=>new Promise(resolve=>setTimeout(resolve, ms));
myForm.onAll("afterRender", async function(ev) {
if (ev.context.parent?.options.type !== "list") return; /* Only for list items */
const item = ev.context.targetNode;
item.classList.add("animated_item");
await delay(1); /* Important: Allow DOM to update */
item.classList.add("ongoing");
});
myForm.onAll("beforeUnrender", async function(ev) {
if (ev.context.parent?.options.type !== "list") return; /* Only for list items */
const item = ev.context.targetNode;
item.classList.remove("ongoing");
/* Await for transition to be finished before item removal: */
await delay(150);
});
Why add animated_item via JavaScript instead of directly in the HTML?
If the class were baked into the template, every item would start hidden even when JavaScript is unavailable. Adding it through the afterRender handler ensures the animation only kicks in when JS is active, so the form degrades gracefully without it.
Why the 1 ms delay in afterRender?
CSS transitions only fire when a property changes after the element is already in the document. If both animated_item and ongoing were added in the same task, the browser would never observe the initial hidden state and the transition would not play. The await delay(1) yields control for one event-loop tick, giving the rendering engine a chance to paint the initial state before ongoing is applied.
Why await delay(150) in beforeUnrender?
SmarkForm awaits the return value of beforeUnrender handlers before detaching the element from the DOM. By returning a promise that resolves after 150 ms (matching the CSS transition-duration), we keep the element visible just long enough for the exit animation to finish.
Applying this globally vs. per-list
myForm.onAll() listens on all components in the form. The guard ev.context.parent?.options.type !== "list" skips anything that is not a direct child of a list β subforms, labels, buttons, etc. The result is that any list added anywhere in the form hierarchy is automatically animated without further wiring.
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
The afterRender handler adds animated_item via JavaScript rather than embedding it directly in the HTML template. This ensures the animation class is only present when JavaScript is active, so the form degrades gracefully if JS is disabled.
The beforeUnrender handler does the reverse: it removes ongoing and returns a Promise delayed by 150 ms β matching the CSS transition duration β so SmarkForm holds the element in the DOM while the exit animation plays out.
Smart value coercion
SmarkForm automatically normalises imported values to match the expected type and shape of each field. This keeps your forms resilient to data-model changes and ensures that what you save is always clean and well-typed.
Scalar-to-array list coercion
When a list field receives a non-array value β a plain string, a number, or an object β it automatically wraps it in a single-item array. This is particularly useful for model migrations: if a field that used to hold a single email string is upgraded to accept a list of emails, old saved data continues to work without any transformation step.
<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": trueon 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:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto 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) andDateobjects on import.<input type="time">exportsHH:MM:SSand acceptsHH:MMon import.- Any field exports
nullwhen 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).
<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": 28to"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) andDateobjects 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:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto 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β¦
<div id="myForm">
<div class="calculator" data-smark='{"type": "input", "name": "display"}'>
<!-- Using singleton pattern here allows us to avoid specifying the context for every button -->
<input
data-smark
type="text"
class="display"
value="0"
pattern="[0-9+\-*\/\(\).]+"
>
<div class="buttons">
<button
data-smark='{"action": "import", "data": "C", "hotkey": "c"}'
class="clear"
>C</button>
<button
data-smark='{"action": "import", "data": "("}'
>(</button>
<button
data-smark='{"action": "import", "data": ")"}'
>)</button>
<button
data-smark='{"action": "import", "data": "/", "hotkey": "/"}'
class="operator"
>Γ·</button>
<button
data-smark='{"action": "import", "data": "7"}'
>7</button>
<button
data-smark='{"action": "import", "data": "8"}'
>8</button>
<button
data-smark='{"action": "import", "data": "9"}'
>9</button>
<button
data-smark='{"action": "import", "data": "*", "hotkey": "*"}'
class="operator"
>Γ</button>
<button
data-smark='{"action": "import", "data": "4"}'
>4</button>
<button
data-smark='{"action": "import", "data": "5"}'
>5</button>
<button
data-smark='{"action": "import", "data": "6"}'
>6</button>
<button
data-smark='{"action": "import", "data": "-", "hotkey": "-"}'
class="operator"
>-</button>
<button
data-smark='{"action": "import", "data": "1"}'
>1</button>
<button
data-smark='{"action": "import", "data": "2"}'
>2</button>
<button
data-smark='{"action": "import", "data": "3"}'
>3</button>
<button
data-smark='{"action": "import", "data": "+", "hotkey": "+"}'
class="operator"
>+</button>
<button
data-smark='{"action": "import", "data": "0"}'
>0</button>
<button
data-smark='{"action": "import", "data": "."}'
>.</button>
<button
data-smark='{"action": "import", "data": "Del"}'
>β</button>
<button
data-smark='{"action": "import", "data": "=", "hotkey": "Enter"}'
class="equals"
>=</button>
</div>
</div>
</div>
#myForm .calculator {
background-color: #333;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
width: 300px;
}
#myForm .display {
width: 100%;
box-sizing: border-box;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
font-size: 24px;
text-align: right;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#myForm .display:invalid {
background-color: #fcc;
}
#myForm .buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5px;
}
#myForm button {
padding: 15px;
font-size: 18px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #555;
color: white;
transition: background-color 0.2s;
}
#myForm button:hover {
background-color: #777;
}
#myForm .operator {
background-color: #f9a825;
}
#myForm .operator:hover {
background-color: #ffb300;
}
#myForm .equals {
background-color: #4caf50;
}
#myForm .equals:hover {
background-color: #66bb6a;
}
#myForm .clear {
background-color: #d32f2f;
}
#myForm .clear:hover {
background-color: #ef5350;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"));const invalidChars = /[^0-9+\-*\/().]+/g;
myForm.on("BeforeAction_import", async (ev)=>{
const prevValue = await ev.context.export();
const key = ev.data;
switch (key) {
case "C":
ev.data = "0"; /* Clear display */
break;
case "Del":
ev.data = prevValue.slice(0, -1) || "0"; /* Remove last character */
break;
case "=":
try {
/* Evaluate expression */
const sanitized = prevValue.replace(invalidChars, '');
ev.data = eval(sanitized);
} catch (e) {
alert("Invalid expression");
ev.preventDefault(); /* Keep existing data */
}
break;
default:
if (prevValue.trim() === "0") {
ev.data = key; /* Replace 0 with new input */
} else {
ev.data = prevValue + key; /* Append to existing value */
};
};
});
π The code in this example is listening to all import actions in the whole form.
This isnβt an issue for this simple example. But if we had other fields in the form (unless they were intended to be additional calculators) would be affected too.
In that case, we could have attached the listener directly to the display field like this:
myForm.onRendered(()=>{
/* Now display field is rendered */
const display = myForm.find("/display");
display.onLocal("BeforeAction_import", async (ev)=>{
/* ... */
});
});
π Using .on()or .onLocal() here is indifferent since inputs have no children.
β¦But in case of forms (or lists of forms) using .on() would have lead to intercept every βBeforeAcction_importβ event in it or its children while .onLocal() will only intercept those triggered by the form itself. Not from any of its descendants.
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
Notice that this calculator has the power superpower for free:
Expressions like
2**10are 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.
<div id="myForm">
<div class="calculator" data-smark='{"type": "input", "name": "display"}'>
<!-- Using singleton pattern here allows us to avoid specifying the context for every button -->
<input
data-smark
type="text"
class="display"
value="0"
pattern="[0-9+\-*\/\(\).]+"
>
<div class="buttons">
<button
data-smark='{"action": "import", "data": "C"}'
class="clear"
>C</button>
<button
data-smark='{"action": "import", "data": "("}'
>(</button>
<button
data-smark='{"action": "import", "data": ")"}'
>)</button>
<button
data-smark='{"action": "import", "data": "/"}'
class="operator"
>Γ·</button>
<button
data-smark='{"action": "import", "data": "7"}'
>7</button>
<button
data-smark='{"action": "import", "data": "8"}'
>8</button>
<button
data-smark='{"action": "import", "data": "9"}'
>9</button>
<button
data-smark='{"action": "import", "data": "*"}'
class="operator"
>Γ</button>
<button
data-smark='{"action": "import", "data": "4"}'
>4</button>
<button
data-smark='{"action": "import", "data": "5"}'
>5</button>
<button
data-smark='{"action": "import", "data": "6"}'
>6</button>
<button
data-smark='{"action": "import", "data": "-"}'
class="operator"
>-</button>
<button
data-smark='{"action": "import", "data": "1"}'
>1</button>
<button
data-smark='{"action": "import", "data": "2"}'
>2</button>
<button
data-smark='{"action": "import", "data": "3"}'
>3</button>
<button
data-smark='{"action": "import", "data": "+"}'
class="operator"
>+</button>
<button
data-smark='{"action": "import", "data": "0"}'
>0</button>
<button
data-smark='{"action": "import", "data": "."}'
>.</button>
<button
data-smark='{"action": "import", "data": "Backspace"}'
>β</button>
<button
data-smark='{"action": "import", "data": "="}'
class="equals"
>=</button>
</div>
</div>
</div>
#myForm .calculator input.display {
caret-color: transparent; /* Hide caret */
}
#myForm .calculator {
background-color: #333;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
width: 300px;
}
#myForm .display {
width: 100%;
box-sizing: border-box;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 10px;
font-size: 24px;
text-align: right;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#myForm .display:invalid {
background-color: #fcc;
}
#myForm .buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 5px;
}
#myForm button {
padding: 15px;
font-size: 18px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #555;
color: white;
transition: background-color 0.2s;
}
#myForm button:hover {
background-color: #777;
}
#myForm .operator {
background-color: #f9a825;
}
#myForm .operator:hover {
background-color: #ffb300;
}
#myForm .equals {
background-color: #4caf50;
}
#myForm .equals:hover {
background-color: #66bb6a;
}
#myForm .clear {
background-color: #d32f2f;
}
#myForm .clear:hover {
background-color: #ef5350;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
const myForm = new SmarkForm(document.getElementById("myForm"));var invalidChars = /[^0-9+\-*\/().]+/g;
function updateDisplay(prevValue, key) {
switch (key.toLowerCase()) {
case "c":
case "delete":
return "0"; /* Clear display */
break;
case "backspace":
return prevValue.slice(0, -1) || "0"; /* Remove last character */
break;
case "=":
case "enter": /* Keyboard enter key */
try {
/* Evaluate expression */
const sanitized = prevValue.replaceAll(invalidChars, '');
return eval(sanitized);
} catch (e) {
return "Error!";
}
break;
default:
if (!! key.match(invalidChars)) {
return prevValue; /* Keep existing data */
};
if (prevValue.replace(/[0\s]+/, "") === "") {
return key; /* Replace 0 with new input */
};
return prevValue + key; /* Append to existing value */
};
};
myForm.on("BeforeAction_import", async (ev)=>{
const prevValue = await ev.context.export();
const key = ev.data;
ev.data = updateDisplay(prevValue, key);
});
myForm.on("keydown", async (ev)=>{
ev.preventDefault();
const prevValue = await ev.context.export();
const key = ev.originalEvent.key;
const data = updateDisplay(prevValue, key);
await ev.context.import(data);
});
π΅οΈ Itβs off-topic but worth to mention the trick of doing !! key.match(invalidChars)) instead of invalidChars.test(key) is not arbitrary since invalidChars is a regex with the global flag set, which makes it suitable for βString.replaceAll()β.
With test(), the internal lastIndex property wonβt be reset making it to fail after first usage.
The !! bit is just stylistic to note we want to evaluate the result of .match() as a boolean.
Every example in this section comes with many of the following tabs:
- HTML: HTML source code of the example.
- CSS: CSS applied (if any).
- JS: JavaScript source code of the example.
- Preview: Live, sandboxed rendering of the example β fully isolated from the page styles.
- Notes: Additional notes and insights for better understanding. Don't miss itβΌοΈ
π οΈ Between the tab labels and the content there is always an edit toolbar:
βοΈ Editβ activates edit mode: each source tab turns into a syntax-highlighted code editor (powered by Ace) pre-filled with the full, merged source. Changes are sandboxed β the original example is not affected.π Include playground editorβ (only visible in edit mode) controls whether the JSON playground editor is included in the preview. When toggled, the HTML and JS editors update instantly so you can see exactly what code is needed to add or remove it. Disabled for this example.βΆοΈ Runβ (only visible in edit mode) re-renders the Preview from the current editor contents and switches to the Preview tab.
In this example we no longer need to define hotkeys since we are directly listening to all keydown events.
If you check the JS tab youβll see that we extracted the key processing logic to a function called updateDisplay() that receives thwo arguments (prevValue and key) to calculate the new value of the display.
It returns null for invalid keystrokes and can report the βError!β condition directly to the display (like a real calculator) since it will be cleared with the next keystroke (no matter which event it comes from).
Then the BeforeAction_import event handler just calls that function and sets the ev.data property with its result.
The keydown event handler does call the updateDisplay() function but:
- It takes the key from the original keydown event.
- Calls the
.preventDefault()method to avoid the keystroke effectively reaching the display. - Programmatically triggers the import action over the display with the new value calculated.
Since now all keyboard strokes are processed by to
updateDisplay()function, this allows us to define handy aliases which will feel more natural in a PC keyboard for some keys like:
Enteras an alias for=Deleteas an alias forCIn the case of the formerly named
Delkey, we just renamed it toBackspaceto match the real key sinceDelwas 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.
<div id="myForm">
<div class="ep">
<p>
<label data-smark>π Event:</label>
<input data-smark name="title" type="text" placeholder="e.g. Sprint Review">
</p>
<p>
<label data-smark>π
Date:</label>
<input data-smark name="date" type="date">
</p>
<p>
<label data-smark>β° Time:</label>
<input data-smark name="time" type="time">
</p>
<fieldset data-smark='{"type":"form","name":"organizer"}'>
<legend data-smark='label'>π€ Organizer</legend>
<p>
<label data-smark>Name:</label>
<input data-smark name="name" type="text">
</p>
<p>
<label data-smark>Email:</label>
<input data-smark name="email" type="email">
</p>
</fieldset>
<div class="ep-list">
<button data-smark='{"action":"removeItem","context":"attendees","hotkey":"Delete","preserve_non_empty":true}' title='Remove empty slots'>π§Ή</button>
<button data-smark='{"action":"addItem","context":"attendees","hotkey":"+"}' title='Add attendee'>β</button>
<strong data-smark='label'>π₯ Attendees:</strong>
<ul data-smark='{"type":"list","name":"attendees","sortable":true,"exportEmpties":false}'>
<li>
<details>
<summary>
<span data-smark='{"type":"label"}' class="bullet">
<span data-smark='{"action":"position"}'>N</span> β°
</span>
<input data-smark type="text" name="name" placeholder="Name">
<button data-smark='{"action":"removeItem","hotkey":"-"}' title='Remove'>β</button>
<button data-smark='{"action":"addItem","hotkey":"+"}' title='Insert here'>β</button>
</summary>
<div class="ep-attendee">
<input data-smark type="email" name="email" placeholder="Email">
<input data-smark type="tel" name="phone" placeholder="Phone">
</div>
</details>
</li>
</ul>
</div>
<p class="ep-hint">π‘ Hold <kbd>Ctrl</kbd> to reveal shortcuts</p>
</div>
</div>
#myForm .ep {
display: flex;
flex-direction: column;
gap: 0.35em;
max-width: 460px;
font-size: 0.95em;
}
#myForm .ep p {
display: flex;
align-items: center;
gap: 0.5em;
margin: 0;
}
#myForm .ep label {
font-weight: 500;
white-space: nowrap;
}
#myForm .ep label:not(.bullet) {
min-width: 4.5em;
}
#myForm .ep input {
padding: 0.3em 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
}
#myForm .ep input[type="text"],
#myForm .ep input[type="email"] {
flex: 1;
}
#myForm .ep fieldset {
border: 1px solid #ddd;
border-radius: 6px;
padding: 0.4em 0.8em 0.6em;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.3em;
}
#myForm .ep fieldset legend {
font-weight: bold;
padding: 0 0.3em;
}
#myForm .ep-list ul {
list-style: none;
padding: 0;
margin: 0.2em 0 0;
display: flex;
flex-direction: column;
gap: 0.25em;
}
#myForm .ep-list ul li {
display: flex;
align-items: flex-start;
gap: 0.3em;
}
#myForm .ep-list ul li details {
width: 100%;
border: 1px solid transparent;
border-radius: 4px;
transition: border-color 0.15s;
}
#myForm .ep-list ul li details[open] {
border-color: #ccc;
padding-bottom: 4px;
}
#myForm .ep-list ul li summary {
display: flex;
align-items: center;
gap: 0.4em;
cursor: default;
user-select: none;
padding: 0.1em 0.2em;
list-style: none;
}
#myForm .ep-attendee {
display: flex;
flex-wrap: wrap;
gap: 0.4em;
padding: 0.3em 0.4em 0.1em 1.5em;
}
#myForm .ep-attendee input {
flex: 1;
min-width: 120px;
}
#myForm .ep-hint {
font-size: 0.82em;
color: #888;
margin: 0.15em 0 0;
}
#myForm .ep-hint kbd {
background: #f4f4f4;
border: 1px solid #ccc;
border-radius: 3px;
padding: 1px 4px;
}
/* Hotkey hints revealed on Ctrl press */
#myForm [data-hotkey]{position:relative}
#myForm [data-hotkey]::after{
content:"Ctrl+" attr(data-hotkey);
position:absolute; top:-1.6em; left:0;
font-size:0.7em;
background:#333; color:#fff;
padding:1px 4px; border-radius:3px;
white-space:nowrap;
}
/* Attendee list item entry/exit animations */
#myForm .ep-list ul li.animated_item {
transform: translateX(-100%);
opacity: 0;
transition: transform 200ms ease, opacity 200ms ease;
}
#myForm .ep-list ul li.animated_item.ongoing {
transform: translateX(0);
opacity: 1;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"title": "Sprint Review",
"date": "2025-03-15",
"time": "10:00:00",
"organizer": {
"name": "Alice Johnson",
"email": "alice@example.com"
},
"attendees": [
{"name": "Bob Smith", "email": "bob@example.com", "phone": "+1 555 200 0001"},
{"name": "Carol White", "email": "carol@example.com", "phone": "+1 555 200 0002"},
{"name": "Dave Brown", "email": "dave@example.com", "phone": "+1 555 200 0003"}
]
}
});const delay = ms=>new Promise(resolve=>setTimeout(resolve, ms));
myForm.onAll("afterRender", async function(ev) {
if (ev.context.parent?.options.type !== "list") return;
const item = ev.context.targetNode;
item.classList.add("animated_item");
await delay(1);
item.classList.add("ongoing");
});
myForm.onAll("beforeUnrender", async function(ev) {
if (ev.context.parent?.options.type !== "list") return;
const item = ev.context.targetNode;
item.classList.remove("ongoing");
await delay(150);
});
π This demo highlights several SmarkForm features at once:
- Foldable rows: Each attendee row uses a native
<details>/<summary>element. Click the βΆ triangle (or anywhere on the row header outside the name field and action buttons) to expand or collapse the extra fields. A<span data-smark='{"type":"label"}'>inside the<summary>acts as the SmarkForm label, making it the drag handle for reordering (shown asβ°). Because the<summary>itself is not the label, the native disclosure triangle and fold/unfold on click are preserved without extra CSS workarounds. - Nested subform: The
organizerfieldset 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: falseso 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 usesDeleteas a context-wide hotkey. - Date & time coercion: The
dateandtimeinputs 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 baredata-smarkon<label>elements wire labels to their fields automatically. - In-form hint: The
π‘ Hold Ctrl to reveal shortcutstext lives inside the form itself rather than as external documentation. - Attendee animations: Items slide in and out via
afterRender/beforeUnrenderevent 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:
β¬οΈ Exportto export the form data to the JSON playground editor.β¬οΈ Importto import data from the JSON playground editor into the form.β»οΈ Resetto reset the form to its default values.β Clearto 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!