The Singleton Pattern
π Table of Contents
Overview
The Singleton Pattern (a.k.a. just βsingletonβ) is a special behaviour of the Scalar field types (the ones deriving from input, such as number, date, color and file) that lets you use any HTML tag as the SmarkForm component of a given scalar type, instead of the native <input>, <select> or <textarea> element.
The only restriction is that the tag must contain exactly one form field inside β though it may contain any number of trigger components.
This lets complex HTML work as a single SmarkForm field.
What Makes a Singleton
A singleton is simply a scalar component whose target node is not the form field itself, but a wrapper tag surrounding it:
<div data-smark='{"type":"input","name":"username"}'>
<label data-smark>π€ User name</label>
<input data-smark>
<button data-smark-action="clear">β Clear</button>
</div>
The wrapper adopts the following rules:
- Exactly one field: the wrapper must contain exactly one data field β an
<input>,<textarea>,<select>, or another scalar field such ascolororfile. It may also contain labels and triggers. Violating this rule raisesNOT_A_SINGLETON(see Restrictions). - Naming: declare the data
nameon the wrapper, in itsdata-smarkoptions or HTMLnameattribute. The singletonβs inner field is intentionally unnamed; anameon that child is ignored. - Scalar import/export: the component imports and exports only the value of the inner field.
- Inheritance: scalar options (e.g.
accept,format,encoding,maskβ¦) declared on the wrapper are automatically inherited by the inner field.
Advantages
π Cleaner code: you no longer need to specify a context for every trigger inside the wrapper β they belong to it naturally (see Avoiding Absolute Context Paths).
π Richer UI: you can build drop zones, icon+label pairs, drag handles and buttons around a single scalar field while still exporting just its value.
π Reusability: a singleton block can be copied verbatim between forms without absolute paths breaking, because every trigger inside resolves relative to the wrapper.
π Lists of scalars: scalar lists ("of":"file", "of":"input"β¦) use singletons as their item template, so each item can carry its own triggers (see Scalar Lists).
Common Use Cases
Complex HTML as a Single Field
The simplest use is wrapping a field with labels, icons and buttons so the whole thing behaves as one component:
<span data-smark='{"type":"color","name":"bgcolor"}'>
<label data-smark>Background</label>
<input type="color" data-smark>
<button data-smark-action="clear" title='Reset'>β</button>
</span>
Try the playable color example below.
Avoiding Absolute Context Paths
Without a singleton, a trigger that needs to act on a field elsewhere in the form must specify an explicit context β often an absolute path like "/color". If the field block is later moved or copied, that absolute path breaks.
Wrapping the field and its triggers in a singleton lets every trigger stay relative to the wrapper, so the block is portable:
<input type="color" name="bgcolor" data-smark>
<!-- ... elsewhere ... -->
<button data-smark='{"action":"clear","context":"/bgcolor"}' title='Reset'>β</button>
vs.
<span data-smark='{"type":"color","name":"bgcolor"}'>
<input type="color" data-smark>
<button data-smark-action="clear" title='Reset'>β</button>
</span>
The second version works no matter where the block is placed β no absolute path to break.
Wrappers and Triggers Inside List Items
Inside a list, each item is a subform. If the list holds scalar items ("of":"input" etc.), there is usually no room for trigger components in the item template β but a singleton provides that room. Each item can then have its own buttons (e.g. to remove that specific item):
See the playable phone numbers list below.
File Fields and Whole-Container Drop & Paste
The file component type makes extensive use of the singleton: wrapping a file field in a drop-zone turns the whole container into the field, so drop and paste are detected over the entire area, and the container delegates actions (download, pickβ¦) to the inner field.
π See the Singleton file example.
Scalar Lists
Lists whose items are scalars (e.g. "of":"file" or "of":"input") use singletons as their item template. This is what allows each item to carry triggers like removeItem, and it is also the recommended way to build lists of files with per-item drop zones (see Files in Lists).
The "of" option itself is just syntax sugar: it declares the itemβs scalar type so the item template doesnβt need its own data-smark attribute just to specify it (see of in the List reference).
Restrictions and Errors
NOT_A_SINGLETON
If a scalar wrapper contains more than one non-trigger field (or none), SmarkForm raises a NOT_A_SINGLETON error at render time. A singleton must hold exactly one field.
SINGLETON_TYPE_MISMATCH
If the inner fieldβs resolved type does not match the scalar type declared on the wrapper, SmarkForm raises SINGLETON_TYPE_MISMATCH. For example, a wrapper declared as "type":"color" must contain one inner field that resolves to the color type.
Relative vs Absolute Context
Inside a singleton, a trigger without context uses the nearest ancestor that implements its action. For example, clear resolves to the scalar wrapper, which delegates to its inner field. This is what makes the pattern portable. Use explicit absolute paths only when you intentionally need root-based addressing; see the Trigger reference.
With the JSON playground visible, absolute trigger
context/targetpaths and absolutemyForm.find('/β¦')calls may not resolve in the live preview. Use relative paths, or omitcontextwhen the trigger is already in the component that handles the action.
Color Reset Example (Singleton)
A playable example of a color field wrapped in a singleton so its Clear button stays relative and the block is portable:
<div id="myForm">
<span data-smark='{"type":"color","name":"bgcolor"}'>
<label data-smark>Background</label>
<input type="color" data-smark>
<button data-smark-action="clear" title='Reset'>β</button>
</span>
</div>
#myForm form span {
display: flex;
align-items: center;
gap: .75rem;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"bgcolor": "#3a7bd5"
}
});
π The β Reset button needs no context: it resolves to the wrapper (the singleton), and the clear action is delegated to the inner color field.
π Try importing a value and then changing the color, or press β Reset to go back to the empty (null) state.
Without the singleton you would have to point the clear action to an absolute context (e.g.
"context":"/bgcolor"), which breaks if the block is moved or copied. The singleton keeps it self-contained.
Phone List Example (Singleton)
A list of scalar items, where each phone input is wrapped in a singleton so the item template can host its own β Remove button:
<div id="myForm">
<ul data-smark='{"type":"list","name":"phones","of":"input","min_items":0}'>
<li data-smark='{"type":"input","name":"phone"}'>
<input type="tel" data-smark placeholder="Phone Number">
<button data-smark-action="removeItem" title='Remove'>β</button>
</li>
</ul>
<button data-smark='{"action":"addItem","context":"phones"}' title='Add Phone'>β</button>
</div>
#myForm ul {
list-style: none;
padding-left: 0;
}
#myForm li {
display: flex;
align-items: center;
gap: .5rem;
margin-bottom: .5rem;
}
const myForm = new SmarkForm(document.getElementById("myForm"), {
"value": {
"phones": [
"+1 555 100 2000",
"+1 555 200 3000"
]
}
});
π Each li is a singleton of type input: it contains one phone field plus its β Remove trigger.
π Because the item template root carries the type, we donβt need data-smark on the <input> child β but itβs kept implicit via data-smark for clarity.
π β Add Phone uses "action":"addItem","context":"phones" to append a new singleton item.
π Note the item name:"phone" is irrelevant for export β the list exports a flat String[] of phones.