Developer Cheatsheet

šŸ“– Table of Contents

Constructor Options

const form = new SmarkForm(element, options);

Note: find() returns null until rendering completes — await form.rendered first.
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-smark syntax Ā· 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

See: Template roles in type_list


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

See: Full import/export docs

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-hotkey attribute on trigger elements.
  • Style with CSS: [data-hotkey]::after { content: attr(data-hotkey); }
  • Triggers with hotkey defined get tabindex="-1" which excludes them from Tab navigation.

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>