Importing and Exporting Data
📖 Table of Contents
Overview
SmarkForm manages all form data through two complementary actions: export and import.
exportreads the current state of a form (or any component inside it) and returns a plain JavaScript value — an object for forms, an array for lists, and a scalar for individual fields.importtakes a plain JavaScript value (or a JSON string) and writes it into the form, updating every field it can match by name.
Both actions work recursively: exporting or importing a form exports or imports all its child components in one call, no matter how deeply they are nested. This means you rarely need to address individual fields from JavaScript — you simply read and write the whole form at once.
Two related actions — reset and clear — build on top of import and are covered in the Default Values, clear, and reset section.
All four actions (
export,import,reset,clear) are available as HTML trigger buttons via thedata-smarkattribute as well as via the JavaScript API.
The export Action
When you call export on a component, SmarkForm collects all the current values in that component’s subtree and returns them as a plain, JSON-serialisable value:
| Component type | Exported value |
|---|---|
form / root | { fieldName: value, … } plain object |
list | [ item, … ] array |
input / scalar fields | The raw field value (string, number, boolean, …) |
Return value structure
The structure of the exported value mirrors the nesting of the form:
<div id="myForm">
<fieldset data-smark='{"type":"form","name":"order"}'>
<legend>Order</legend>
<p>
<label data-smark>Customer:</label>
<input data-smark type="text" name="customer">
</p>
<p>
<label data-smark>Notes:</label>
<textarea data-smark name="notes"></textarea>
</p>
<ul data-smark='{"type":"list","name":"items","min_items":0}'>
<li>
<input data-smark type="text" name="product" placeholder="Product">
<input data-smark type="number" name="qty" placeholder="Qty">
<button data-smark='{"action":"removeItem"}'>➖</button>
</li>
</ul>
<button data-smark='{"action":"addItem","context":"items"}'>➕ Add item</button>
</fieldset>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
value: {
"order": {
"customer": "Jane Smith",
"notes": "Please wrap as a gift",
"items": [
{
"product": "Widget A",
"qty": 3
},
{
"product": "Widget B",
"qty": 1
}
]
}
}
});
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.♻️ Resetto reset 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 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.
Click ⬇️ Export to see the exported JSON. The structure of the exported object matches the form structure exactly:
{
"order": {
"customer": "Jane Smith",
"notes": "Please wrap as a gift",
"items": [
{ "product": "Widget A", "qty": 3 },
{ "product": "Widget B", "qty": 1 }
]
}
}
The exportEmpties option
By default, SmarkForm strips empty items from list exports. A list item is considered empty when all its fields are empty (null, undefined, or empty string).
This behaviour is controlled by the exportEmpties option on the list component:
exportEmpties value | Behaviour |
|---|---|
false (default) | Empty list items are omitted from the exported array |
true | All items, including empty ones, are included in the export |
<!-- Empty items will be included in the export -->
<ul data-smark='{"type":"list","name":"slots","exportEmpties":true}'>
<li>
<input data-smark type="text" name="value">
</li>
</ul>
exportEmptiesis inherited — a child component inherits the value from its nearest ancestor that sets it explicitly. This means you may need to explicitly setexportEmpties:falseon a nested list to override an ancestor’sexportEmpties:true. For example:<!-- Outer list: export all items, including empty ones --> <ul data-smark='{"type":"list","name":"sessions","exportEmpties":true}'> <li> <!-- Inner list: strip empties regardless of the outer setting --> <ul data-smark='{"type":"list","name":"tags","exportEmpties":false}'> <li><input data-smark type="text" name="tag"></li> </ul> </li> </ul>
The exportEmpties option is particularly useful in “save progress” scenarios where you want to preserve the user’s position in a list even if they haven’t filled in all the items yet.
The import Action
import takes a value and writes it into the component. For a form component it distributes each key of the object to the matching child field; for a list it adds or removes items to match the length of the incoming array, then fills each item.
Accepted data formats
The import action accepts:
- A plain JavaScript object for
formcomponents - A JSON string that parses to a plain object — SmarkForm parses it automatically
- An array for
listcomponents - A scalar value (string, number, boolean) for input fields
Fields that are present in the form but absent from the imported data object receive
undefined, which triggers their own reset to default. Fields present in the data object but absent from the form are silently ignored.
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input data-smark type="text" name="name">
</p>
<p>
<label data-smark>Email:</label>
<input data-smark type="email" name="email">
</p>
<p>
<label data-smark>Role:</label>
<input data-smark type="text" name="role">
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
value: {
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "Developer"
}
});
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.♻️ Resetto reset 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 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.
Paste the following JSON into the editor and click ⬆️ Import to see it load into the form:
{"name":"Bob Smith","email":"bob@example.com","role":"Designer"}
The setDefault option
By default, every successful import call updates the component’s defaultValue. This means that after an import, clicking ♻️ Reset restores the imported data rather than the original HTML-defined defaults.
You can opt out of this behaviour by passing setDefault: false:
<!-- Import without updating the reset target (preview mode) -->
<button data-smark='{"action":"import","setDefault":false}'>Preview</button>
<!-- Import and make this the new reset target (load mode) -->
<button data-smark='{"action":"import"}'>Load Data</button>
Summary of setDefault behaviour:
| Call | Updates default? | What reset() restores |
|---|---|---|
import(data) | Yes | The newly imported data |
import(data, {setDefault:false}) | No | The previous default |
import(undefined) | No | The current default (unchanged) |
clear() | No | The current default (unchanged) |
reset() | No | The current default |
setDefaultapplies only to the originating field: when a form imports data withsetDefault: true, only that form updates its own default. Child components (nested forms, lists, and leaf fields) always receivesetDefault: falseso that their own defaults are not affected.
The focus option
When import is invoked through a trigger button, SmarkForm automatically focuses the form after import. When called programmatically, focus is off by default. Pass focus: true to move keyboard focus after a programmatic import:
await myForm.actions.import(data, { focus: true });
Default Values, clear, and reset
Setting defaults via value
You can pre-populate a form’s default values in HTML using the value property inside data-smark:
<div data-smark='{"name":"profile","value":{"name":"Anonymous","role":"guest"}}'>
<input data-smark type="text" name="name">
<input data-smark type="text" name="role">
</div>
This sets defaultValue for the profile form. Calling reset() on it always restores {"name":"Anonymous","role":"guest"}.
Individual input fields can also have a default via the HTML value attribute:
<input data-smark type="text" name="country" value="US">
Do not set both the HTML
valueattribute and thedata-smarkvalueoption on the same element at the same time — SmarkForm will throw aVALUE_CONFLICTerror during render.
How import updates the default
After a successful import(data) call (with setDefault: true, the default), SmarkForm re-exports the component with exportEmpties: true and stores the result as the new defaultValue:
// After this call, reset() will restore {name:"Alice",role:"admin"}
await myForm.actions.import({ name: "Alice", role: "admin" });
Using exportEmpties: true when capturing the new default ensures that reset() restores the exact same list structure — including empty slots — that was in place right after the import.
Comparing clear and reset
Both clear and reset are built on top of import but behave differently:
| Action | What it does | Updates default? |
|---|---|---|
reset | Imports the current defaultValue | No |
clear | Imports the type-level empty value ({} for forms, [] for lists, "" for inputs) | No |
Use reset to undo user changes and return to the last loaded state.
Use clear to blank every field without regard to defaults — for example, a “New record” button.
<div id="myForm">
<div data-smark='{"type":"form","name":"task","value":{"title":"Buy groceries","priority":"medium"}}'>
<p>
<label data-smark>Title:</label>
<input data-smark type="text" name="title">
</p>
<p>
<label data-smark>Priority:</label>
<input data-smark type="text" name="priority">
</p>
<p>
<button data-smark='{"action":"clear","context":"task"}'>❌ Clear</button>
<button data-smark='{"action":"reset","context":"task"}'>♻️ Reset</button>
</p>
</div>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
The task form is initialized with value defaults.
- Modify the fields.
- Click ❌ Clear — all fields become empty.
- Click ♻️ Reset — the original defaults are restored.
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.♻️ Resetto reset 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 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.
Piping Data Between Components
SmarkForm makes it easy to copy data from one component to another using the target option on export and import triggers. This avoids writing any JavaScript for common copy-data workflows.
Using target with export
When an export trigger has a target property, SmarkForm automatically imports the exported value into the target component after the export completes:
<!-- Export "source" and automatically import the result into "destination" -->
<button data-smark='{
"action": "export",
"context": "source",
"target": "/destination"
}'>📋 Copy</button>
This is equivalent to writing the following JavaScript:
const value = await source.export();
await destination.import(value);
Using target with import
Conversely, when an import trigger has a target property, SmarkForm exports from the target first and uses that value as the data to import:
<!-- Export "source", then import the result here -->
<button data-smark='{
"action": "import",
"target": "/source"
}'>📥 Load from source</button>
This is useful inside list items for the “duplicate” pattern — export the previous sibling and import it into the current item.
Chaining export and import
The following example shows a practical copy-between-sections workflow. The “Copy to shipping” button exports its context (the billing address) subform directly into its target (the shipping address) subform:
<div id="myForm">
<fieldset data-smark='{"type":"form","name":"billing"}'>
<legend>Billing address</legend>
<p>
<label data-smark>Street:</label>
<input data-smark type="text" name="street">
</p>
<p>
<label data-smark>City:</label>
<input data-smark type="text" name="city">
</p>
<p>
<label data-smark>Postcode:</label>
<input data-smark type="text" name="postcode">
</p>
</fieldset>
<p>
<button data-smark='{
"action":"export",
"context":"billing",
"target":"../shipping"
}'>📋 Copy to shipping</button>
</p>
<fieldset data-smark='{"type":"form","name":"shipping"}'>
<legend>Shipping address</legend>
<p>
<label data-smark>Street:</label>
<input data-smark type="text" name="street">
</p>
<p>
<label data-smark>City:</label>
<input data-smark type="text" name="city">
</p>
<p>
<label data-smark>Postcode:</label>
<input data-smark type="text" name="postcode">
</p>
</fieldset>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"), {
value: {
"billing": {
"street": "10 Downing Street",
"city": "London",
"postcode": "SW1A 2AA"
},
"shipping": {
"street": "",
"city": "",
"postcode": ""
}
}
});
Fill in the billing address, then click 📋 Copy to shipping to copy the data to the shipping address with no JavaScript 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.♻️ Resetto reset 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 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.
The
contextpath is resolved relative to where the trigger is placed in the form hierarchy, whiletargetpaths are resolved relative to the effective context of the action.👉 More info at Form Traversing chapter.
Notice that the same effect could have been seamlessly achieved with an
importaction in the reverse direction.<button data-smark='{ "action":"import", "context":"shipping", "target":"../billing" }'>📋 Copy to shipping</button>…or, in both cases, we could just reverse only the action (or only the direction) to achieve Copy from shipping to billing instead of the other way around.
Programmatic API
Using component.actions.export() and component.actions.import()
To export or import data from JavaScript you call the action through the component’s .actions map. This goes through the full @action wrapper, which fires BeforeAction_* and AfterAction_* events and applies all the standard focus/silent/data flow logic:
// Wait for the form to finish rendering before using it
await myForm.rendered;
// Export the whole form
const data = await myForm.actions.export();
console.log(data); // { name: "Alice", … }
// Import data into the form
await myForm.actions.import({ name: "Bob", email: "bob@example.com" });
// Import without updating the default (preview mode)
await myForm.actions.import(data, { setDefault: false });
// Import a specific sub-form
const addressForm = myForm.find("/address");
await addressForm.actions.import({ street: "123 Main St", city: "Springfield" });
Always
await myForm.renderedbefore callingfind()or accessing components programmatically. SmarkForm builds its internal component map asynchronously — calls made before rendering is complete will returnnull.
Calling prototype methods directly
For internal use — such as inside event handlers or other action implementations — you can call the prototype method directly, bypassing the @action wrapper:
// Bypasses events, focus defaults, and BeforeAction cancellation:
const data = await myForm.export();
await myForm.import({ name: "Alice" }, { silent: true });
Direct prototype calls are lower overhead and are the right choice when you are already inside an action handler and do not want to re-fire lifecycle events. However, you must then manage options.silent and options.focus explicitly.
See the Event Handling chapter for a deeper explanation of the @action wrapper and event lifecycle.
Common Patterns
Loading initial data from a server
The typical pattern for pre-populating a form from the server is to wait for the form to finish rendering and then call import:
const myForm = new SmarkForm(document.getElementById("myForm"));
myForm.on("AfterAction_import", (ev) => {
console.log("Form loaded with:", ev.data);
});
await myForm.rendered;
const response = await fetch("/api/record/42");
const data = await response.json();
await myForm.actions.import(data); // data becomes the new default
Because import defaults to setDefault: true, the user can later click ♻️ Reset to restore the loaded record without re-fetching.
Submitting form data to a backend
The recommended pattern is to place an export trigger on the page and listen for AfterAction_export:
<button data-smark='{"action":"export"}'>💾 Save</button>
const myForm = new SmarkForm(document.getElementById("myForm"), {
async onAfterAction_export(ev) {
const payload = ev.data; // Plain JSON-serialisable object
const res = await fetch("/api/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
},
});
To run validation before allowing the export, use
onBeforeAction_exportand callev.preventDefault()to cancel the action. See the Event Handling chapter for more details.
Save and restore draft data
You can persist draft data in localStorage and restore it the next time the user opens the page:
const myForm = new SmarkForm(document.getElementById("myForm"));
await myForm.rendered;
// Restore draft (without making it the reset default)
const draft = localStorage.getItem("myForm.draft");
if (draft) {
await myForm.actions.import(JSON.parse(draft), { setDefault: false });
}
// Auto-save on every export
myForm.on("AfterAction_export", (ev) => {
localStorage.setItem("myForm.draft", JSON.stringify(ev.data));
});
Using
setDefault: falsewhen restoring a draft ensures that ♻️ Reset still returns to the form’s HTML-defined defaults, not the draft. This lets the user discard a draft by pressing Reset.