Developer Cheatsheet
📖 Table of Contents
Constructor Options
const form = new SmarkForm(element, options);
Note:
find()returnsnulluntil rendering completes —await form.renderedfirst.
See: Full constructor reference
| Option | Type | Default | Description |
|---|---|---|---|
value | Object | {} | Initial data |
customActions | Object | {} | Custom action implementations |
autoId | Boolean/String/Function | false | Auto-generate element IDs |
smark_mixin_allowExternal | String | "block" | External mixin fetching policy |
smark_mixin_allowLocalScripts | String | "block" | Local mixin <script> policy |
smark_mixin_allowSameOriginScripts | String | "block" | Same-origin mixin script policy |
smark_mixin_allowCrossOriginScripts | String | "block" | Cross-origin mixin script policy |
enableJsonEncoding | Boolean | false | Enable enctype |
exportEmpties | Boolean | false | Export empty items |
keyStyle | "bracket" / "dot" | "bracket" | Form encoding key style |
arrayStyle | "repeat" / "index" | "repeat" | Form encoding array style |
focus_on_click | Boolean | true | Focus containers on click |
on* | Function | — | Event handlers (on_click, onLocal_AfterAction_export, etc.) |
The data-smark Attribute
| Form | Example | When |
|---|---|---|
| Full JSON | data-smark='{"type":"list","name":"items","of":"input"}' | Full control |
| Type shorthand | data-smark="list" | Defaults for that type |
| Bare attribute | data-smark | Type/name inferred from HTML |
data-smark="data-smark" | data-smark="data-smark" | Treated as empty options |
data-smark is optional on: root element (implicit form type, also gets options from constructor), list item template (implicit for the item role, type inferred or set in the “of” option of the list).
See:
data-smarksyntax · Shorthand forms
Component Types
| Type | Data Type | Default | Tag Inference |
|---|---|---|---|
form | JSON object | {} | <form>, <div>, any container |
list | JSON array | [] | <ul>, <ol>, <table>, <thead>, <tbody>, <tfoot> |
input | String | "" | <input>, <textarea>, <select> |
file | Data-URL string or object / null | null | <input type="file"> or singleton container |
number | Number / null | null | <input type="number"> |
date | ISO date / null | null | <input type="date"> |
time | HH:mm:ss / null | null | <input type="time"> |
datetime-local | ISO datetime / null | null | <input type="datetime-local"> |
color | Hex color / null | null | <input type="color"> |
radio | Selected value / null | null | Radio button group |
trigger | N/A | N/A | Any element with action property |
label | N/A | N/A | <label> |
| mixin | Varies | Varies | data-smark='{"type":"url#templateId"}' |
Singleton: input-derived type on a container wraps exactly one real field:
<li data-smark="input">
<input data-smark type="tel">
<button data-smark='{"action":"removeItem"}'>✕</button>
</li>
List Template Roles
Every direct child of a list is a template — removed from DOM on init. Set via data-smark='{"role":"<role>"}'.
| Role | Purpose | Cloned? | Notes |
|---|---|---|---|
item (default) | Repeating item | Yes | Required |
empty_list | Shown when list empty | No | Auto-managed |
header | Prepended once | No | No data fields allowed |
footer | Appended once | No | No data fields allowed |
placeholder | DOM filler for fixed-width grids | Yes | Only when max_items set |
separator | Between adjacent items | Yes | Removed when only 1 item |
last_separator | Between 2nd-last & last | Yes | Falls back to separator |
Actions
| Action | Target Type | Description |
|---|---|---|
export | Any | Return current value as JSON |
import | Any | Set value from JSON data |
reset | Any | Restore defaultValue |
clear | Any | Reset to type-level emptyValue |
addItem | list | Add new item |
removeItem | list | Remove target item(s) |
submit | form | Submit form data via HTTP |
count | list | Get/set total item count |
position | list item | Get/set 1-based index |
move | list (sortable) | Move item within/across lists |
| custom | any | Via customActions |
Signature: async actionName(data, options = {})
Context & Target Resolution
Natural context: walks ancestors to find first component implementing the action.
Explicit context: resolved from the trigger’s enclosing field component (triggers themselves are not path nodes).
<button data-smark='{"action":"clear"}'>Clear</button>
<button data-smark='{"action":"export","context":"demo"}'>Export Demo</button>
<button data-smark='{"action":"export","context":"/shipping"}'>Export Shipping</button>
See: Path syntax · Context & target
| Path | Resolves to |
|---|---|
"name" | Sibling component named “name” |
"/" | Root form |
"." | Current field |
"..fieldname" | Field in parent scope (no / after ..) |
".-1" | Previous list item sibling |
".+1" | Next list item sibling |
"../sibling" | Sibling of parent |
"items/*" | All children matching wildcard |
Target semantics:
| Combination | Result |
|---|---|
export + target | Export context → import into target |
import + target | Export from target → import into context |
removeItem + target:"*" | Remove ALL items |
removeItem + preserve_non_empty:true | Remove only empty items |
Pitfall: target:"shipping" looks for a child of context. Use target:"../shipping" for sibling.
Event System
| Method | Scope | Bubbles? |
|---|---|---|
component.on(ev, handler) | Component + ancestors | If event says bubbles:true |
component.onLocal(ev, handler) | Only this component | Never |
component.onAll(ev, handler) | Component + ancestors | Always |
See: Event system docs · Constructor shorthand · Action lifecycle
DOM events (capture-phase on root, dispatched to target): keydown, keyup, input, change, focus, blur, click, dblclick, mousedown, mouseup …
Synthetic: focusenter, focusleave, BeforeAction_<name> (preventable), AfterAction_<name>, beforeRender, afterRender, beforeUnrender, removeItem_beforeRender, removeItem_afterRender
Constructor shorthand:
const f = new SmarkForm(element, {
onLocal_AfterAction_export(ev) {},
on_click(ev) {},
onAll_focus(ev) {},
onBeforeAction_import(ev) { ev.preventDefault(); },
});
Modify data in BeforeAction:
component.onLocal("BeforeAction_import", (ev) => {
ev.data = transformData(ev.data);
});
Data Import / Export
await form.export(); // Full form
await form.find("/username").export(); // Single field
await form.import({ name: "Alice" }); // Import data
| Call | Updates default? | reset() restores |
|---|---|---|
import(data) | Yes | data |
import(data, {setDefault:false}) | No | Previous default |
import(undefined) | No (skipped) | Current default |
clear() | No | Default unchanged |
reset() | No | Current default |
exportEmpties inherited from nearest ancestor that sets it:
<div data-smark='{"type":"list","name":"outer","exportEmpties":true}'>
<div data-smark='{"type":"list","name":"inner","exportEmpties":false}'></div>
</div>
Field-level JSON round-trips: add {"format":"json"} (or legacy encoding:"json") to any <input>, <textarea> or <select> to export a parsed JS value and import by serializing to JSON text:
<textarea name="metadata" data-smark='{"format":"json"}'></textarea>
Applies to the field’s own import/export; other fields keep their raw values.
See: JSON format
Copy pattern:
const data = await form.find("/billing").export();
await form.find("../shipping").import(data);
File Fields
{"type":"file"} on a real <input> (tuned to a text field showing the editable file name) or on a singleton container wrapping exactly one inner field:
<input data-smark='{"type":"file","name":"cv","format":"json","encoding":"hex"}'>
<div class="drop-zone" data-smark='{"type":"file","name":"cv"}'>… one inner field …</div>
| Option | Default | Effect |
|---|---|---|
accept | "" | Native filter — also applied to drops/pastes |
format | "raw" | "raw" → data-URL string · "json" → {name,type,size,lastModified,data} |
encoding | "base64" | Payload byte encoding ("base64" / "base64url" / "hex") for object data and bare-string imports; data-URLs are always base64 |
smark_file_open | true | click / Shift+Space open the OS picker |
smark_file_drop | true | Drag & drop |
smark_file_paste | true | Paste |
- Export: raw →
data:image/png;name=photo.png;size=123456;lastModified=1690000000000;base64,…; json → structured object; empty →null. - Import: data-URL string, (partial) object, bare payload string, or JSON string — all accepted;
sizeis recomputed from the payload. - Download (action):
{"action":"download","context":"/cv"}triggers a real browser download of the stored bytes; empty field → no-op. Name precedence:filenametrigger option > edited visible name > stored name. - Singleton: drop/paste detected over the whole container; download delegates to the inner field.
- Lists:
{"type":"list","name":"photos","of":"file"}—addItemopens a multi-file picker (multiple:falseforces single); OS drops append items (list-levelfileDrop:falsedisables).
See: File field type
Image Fields
{"type":"image"} on a native <img> (the picture is the field), or on a singleton container (any tag wrapping exactly one inner <img>). Extends file — same value contract, acquisition and download action.
<img data-smark='{"type":"image","name":"photo"}'>
<figure data-smark='{"type":"image","name":"photo"}'>…<img data-smark>…<figcaption contenteditable>…</figcaption>…</figure>
| Option | Default | Effect |
|---|---|---|
accept | "image/*" | Native filter — also applied to drops/pastes |
placeholder | chessboard SVG | Empty-state image (URL/data URL); false = leave src empty |
image_resize | — | Acquired images stretched to exact [w,h] / {width,height} / square number |
image_maxSize | — | Downscale-only cap (fit within the box, aspect preserved) |
image_format | — | Convert on acquire: "jpeg" / "jpg" / "png" / "webp" / "avif" |
image_enforce | "warn" | "strict"·"hard"·"warn"·"ignore" — verdict for unmet/unverifiable requirements |
smark_image_validate | true | Decode-test acquired files (broken/non-image → silently rejected) |
smark_image_clearOnDelete | true | Delete/Backspace clear the value (never while editing a caption) |
smark_image_open / smark_image_drop / smark_image_paste | true | Acquisition toggles (smark_file_* honored as fallbacks) |
- Inference: bare
<img data-smark>is inferred asimage;<input type="image">raisesIMAGE_TYPE_ON_INPUT. - Display:
src= stored bytes while set, placeholder when empty; the field never toucheswidth/height/alt/class— reserve layout space in the markup. - Export/import: identical to
file(data-URL string default /{name,type,size,lastModified,data}with"format":"json"; embedded-only, no width/height metadata). - Name: on
<figure>wrappers and gallery items afigcaption contenteditablemirrors/edits the storedname(edited caption > stored name). Without one, the stored name governs. - Notices: unmet/unverifiable requirements dispatch a bubbling
smark:imageNoticeevent (+ toast unlesspreventDefault()d). - Lists:
{"type":"list","name":"gallery","of":"image"}→ flat array of data URLs;addItemmulti-picks and decode-filters the batch; OS drops append items (also on an existing item).
See: Image field type
API Methods
Every field component:
See: Import/export · Path traversal · Events
Action methods
| Method | Description |
|---|---|
export(data, options) | Return current value |
import(data, options) | Set value from data |
clear(options) | Reset to type-level empty |
reset(options) | Restore defaultValue |
Utilities & Introspection
| Method | Description |
|---|---|
isEmpty() | true if no meaningful data |
find(path) | Navigate by path string |
getPath() | Absolute path (e.g. "/address/street") |
focus(options) | Focus the field |
moveTo() | Scroll to and highlight |
onRendered(callback) | Run after render |
on(ev, handler) | Listen with bubbling |
onLocal(ev, handler) | Listen target-phase only |
onAll(ev, handler) | Listen always-bubbling |
emit(evType, data, preventable) | Emit custom event |
getTriggers(actionName, limit) | Find triggers targeting this component |
updateId() | Auto-generate id from autoId |
inheritedOption(name, default) | Walk ancestors for option value |
Hotkeys
Triggers with a hotkey property reveal hints when Ctrl (level 1) or Ctrl+Alt (level 2) is pressed:
<button data-smark='{"action":"addItem","hotkey":"+"}'>Add</button>
<button data-smark='{"action":"removeItem","hotkey":"-"}'>Remove</button>
- Hints appear as
data-hotkeyattribute on trigger elements. - Style with CSS:
[data-hotkey]::after { content: attr(data-hotkey); } - Triggers with hotkey defined get
tabindex="-1"which excludes them fromTabnavigation.
See: Hotkeys docs
Form Submission
See: Form submission · Encoding options
<form action="/api/submit" method="post">
<button type="submit" data-smark='{"action":"submit"}'>Submit</button>
</form>
- Enter navigates fields — does not submit.
- Only explicit click on submit-type buttons triggers submission.
- Supports
application/json(enableJsonEncoding: true). - Respects
formaction,formmethod,formenctype,formtarget.
Quick Templates
Simple form
<div id="myForm">
<label data-smark>Name:</label>
<input name="name" data-smark>
<label data-smark>Email:</label>
<input name="email" type="email" data-smark>
<button data-smark='{"action":"clear"}'>Clear</button>
<button data-smark='{"action":"export"}'>Export</button>
</div>
<script>new SmarkForm(document.getElementById("myForm"));</script>
Nested form
<div data-smark='{"type":"form","name":"address"}'>
<input name="street" data-smark>
<input name="city" data-smark>
</div>
List with all template roles
<div data-smark='{"type":"list","name":"items","min_items":0,"max_items":5}'>
<div><input name="name" data-smark></div>
<div data-smark='{"role":"empty_list"}'>No items yet.</div>
<div data-smark='{"role":"header"}'><b>Items:</b></div>
<div data-smark='{"role":"footer"}'>
<button data-smark='{"action":"addItem","hotkey":"+"}'>Add</button>
</div>
<hr data-smark='{"role":"separator"}'>
</div>
Trigger with context and target
<button data-smark='{"action":"export","context":"billing","target":"../shipping"}'>Copy to Shipping</button>
<button data-smark='{"action":"removeItem","context":"myList","target":"*"}'>Remove All Items</button>
Sortable list
<ul data-smark='{"type":"list","name":"pets","sortable":true,"min_items":0}'>
<li>
<label data-smark title="Drag to reorder">☰</label>
<input name="name" data-smark>
<button data-smark='{"action":"removeItem"}'>✕</button>
</li>
</ul>
Tooltip mixin with CSS
A reusable mixin that wraps a field with a CSS-only tooltip:
<template id="tooltipField">
<label data-smark>
<span id="labelText">Field</span>
<input data-smark type="text">
<span class="tti">ⓘ</span>
<span id="tipText" hidden>tooltip</span>
</label>
<style>
label { position: relative; }
.tti { cursor: help; margin-left: .3em; opacity: .6; }
.tti:hover { opacity: 1; }
.tti:hover + #tipText { display: inline !important;
position: absolute; bottom: 100%; left: 0;
background: #333; color: #fff; padding: .2em .4em;
border-radius: .3em; white-space: nowrap; }
</style>
</template>
<div data-smark='{"type":"#tooltipField","name":"email"}'>
<span data-for="labelText">Email</span>
<span data-for="tipText">Enter your email address</span>
</div>