Showcase
This section provides a series of working examples to demonstrate the capabilities of SmarkForm without diving into code details.
It highlights key features through examples, using short and readable code that prioritizes clarity over UX/semantics. The examples use minimal or no CSS (if any youβll find it at the CSS tab) to show layout independence.
They go step by step from the most basic form to more advanced and fully featured ones.
π If you are eager to to see the full power of SmarkForm in action, you can check the π Examples section first.
π Nonetheless, if you are impatient to get your hands dirty, the π Quick Start is there for you.
π Table of Contents
Basics
Just a Form
Fields auto-register from HTML attributes β no JavaScript beyond initialization. Try editing values in the preview or clicking the β¬οΈ Export button to edit the JSON in the playground editor at the bottom.
<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.β»οΈ Resetin forms prefilled with sample data, resets 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.
Multiple-Level Nesting
This single HTML example packs a surprising range of SmarkForm features: multiple-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.
The demo looks richer than raw HTML would suggest: it ships with pre-loaded JSON data so the form starts populated, and a few lines of CSS to make the layout appealing. Check the CSS and JS tabs to see both.
<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: rgba(165, 165, 165, .25);
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: rgba(168, 168, 168, .25); 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: rgba(168, 168, 168, .25);
}
#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: rgba(165, 165, 165, .25); 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: rgba(165, 165, 165, .25); }
#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: rgba(168, 168, 168, .25); 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: rgba(168, 168, 168, .25); 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.β»οΈ Resetin forms prefilled with sample data, resets 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
SmarkForm fields can be nested to any depth β the only limit is your applicationβs design.
All the playground editor controls (Export, Import, Reset, Clear) are SmarkForm triggers themselves. Check the βοΈ Edit tab to see how the editor scaffold is constructed.
See Form Types for details on form nesting and the playground architecture.
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.β»οΈ Resetin forms prefilled with sample data, resets 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.β»οΈ Resetin forms prefilled with sample data, resets 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"), {"smark_mixin_allowLocalScripts":"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.β»οΈ Resetin forms prefilled with sample data, resets 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
New list items copy data from the previous sibling when their source is set to ".-1". Combined with min_items: 0 and the empty_list template role, you can build list items that start empty but inherit useful defaults.
See List Types for
source,min_items, and template roles.
A note on empty values
The exportEmpties option (default false) omits blank items from exported data. This behaviour inherits down to nested lists β see List Types for the full inheritance rules.
Nesting Mixins
Mixin templates can reference other mixins via data-for slots. The example above composes #periodItem (referencing #scheduleRow with a data-for attribute) to reuse the scheduling mixin inside the period mixin.
Duplicate <style> elements are automatically de-duplicated by SmarkForm.
See Mixin Types for composition, scripts, and style handling.
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
SmarkFormβs export/import buttons are triggers with context and target properties. See Data Import and Export for details.
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.β»οΈ Resetin forms prefilled with sample data, resets 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.β»οΈ Resetin forms prefilled with sample data, resets 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
A hotkeyβs effect is determined by the button it is attached to. The same hotkey (Ctrl+- in this example) can trigger different actions depending on which list itemβs button is in focus. SmarkForm resolves conflicts by preferring the innermost matching context.
See Hotkeys for context sensitivity and conflict resolution.
<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.β»οΈ Resetin forms prefilled with sample data, resets 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
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.β»οΈ Resetin forms prefilled with sample data, resets 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
Press Enter to move forward to the next field, Shift+Enter to go back. Buttons with hotkeys are automatically excluded from the Tab flow to keep navigation fluid.
See Keyboard Navigation for the full reference.
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.β»οΈ Resetin forms prefilled with sample data, resets 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βs afterRender and beforeUnrender lifecycle events make it easy to add CSS entry/exit animations to list items. The technique uses class toggling with onAll() so every list in the form is animated automatically.
<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.
See Animations for the full guide including the 1ms delay rationale, global wiring, and graceful degradation details.
Smart value coercion
SmarkForm automatically normalises imported values to match the expected type and shape of each field. Lists coerce scalar values to single-item arrays; number, date and time fields normalise string inputs to their native types; JSON-encoded fields round-trip objects through textareas.
See Value Coercion for the complete rules and interactive demos.
Field Masking
SmarkFormβs declarative masking API lets you integrate any external input-masking library. Define a mask factory once and reference it by name in data-smark β no post-construction setup needed.
This example uses IMask.js to format a credit card number with spaces every 4 digits. The factory returns a wrapper whose unmaskedValue getter returns null when the card is incomplete, so export() never includes partially-typed data.
<div id="myForm">
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<div data-smark='{"type":"form","name":"payment"}'>
<p>
<label data-smark>Card Number:</label>
<input data-smark='{"type":"number","name":"cardNumber","mask":"card"}'>
</p>
</div>
</div>
input:invalid {
outline: 1px solid #d4c070;
outline-offset: -1px;
}
SmarkForm.registerMask("card", (node) => {
node.placeholder = "0000 0000 0000 0000";
node.inputMode = "numeric";
let showLazy = true;
const imask = new IMask(node, {
mask: "0000 0000 0000 0000",
lazy: true,
});
let prevValue = node.value;
let lastKey = "";
node.addEventListener("keydown", (e) => { lastKey = e.key; });
node.addEventListener("input", () => {
const hasContent = imask.masked.unmaskedValue.length > 0;
if (hasContent && showLazy) {
showLazy = false;
imask.updateOptions({
mask: "0000 0000 0000 0000",
lazy: false,
placeholderChar: "_",
});
} else if (!hasContent && !showLazy) {
showLazy = true;
imask.updateOptions({
mask: "0000 0000 0000 0000",
lazy: true,
});
}
if (node.value === prevValue && node === document.activeElement) {
if (lastKey !== "Backspace" && lastKey !== "Delete") {
node.style.boxShadow = "0 0 0 2px #f80";
setTimeout(() => node.style.boxShadow = "", 250);
}
}
prevValue = node.value;
const raw = imask.masked.unmaskedValue;
const showError = raw.length > 0 && !imask.masked.isComplete;
node.setCustomValidity(showError ? "Please enter the full 16-digit card number" : "");
});
return {
get unmaskedValue() {
return imask.masked.isComplete ? imask.masked.unmaskedValue : null;
},
set unmaskedValue(v) {
// Use IMask's top-level setter (syncs the DOM via updateControl).
imask.unmaskedValue = v;
// IMask diffs an `input` event against its last saved selection, which
// is only refreshed on focus/keydown. Since SmarkForm dispatches an
// `input` event after every import, a stale selection would make IMask
// read the imported value as a duplicate insertion (e.g. "4111" β
// "4111 4111"). Sync caret and selection to the end instead.
const len = node.value.length;
node.setSelectionRange(len, len);
imask._selection = { start: len, end: len };
},
};
});
const myForm = new SmarkForm(document.getElementById("myForm"));
A fully-worked credit card field: IMask with lazy switching (placeholder β underscore fill), inputMode: "numeric" for mobile keyboards, orange blink on rejected keystrokes, and a wrapper that returns null for incomplete numbers so they are never exported.
Want to learn more? See Field Masking.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 masking? See the full reference in Field Masking.
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: rgba(165, 165, 165, .25);
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.β»οΈ Resetin forms prefilled with sample data, resets 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!