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 |
allowExternalMixins | String | "block" | External mixin fetching policy |
allowLocalMixinScripts | String | "block" | Local mixin <script> policy |
allowSameOriginMixinScripts | String | "block" | Same-origin mixin script policy |
allowCrossOriginMixinScripts | 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> |
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>
See: Full type reference Ā· Singleton pattern
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>
Copy pattern:
const data = await form.find("/billing").export();
await form.find("../shipping").import(data);
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>