Quick Start
A SmarkForm form can be created by following a few simple steps:
This section is meant to be educational. If you are eager to get your hands dirty, ๐ go straight to the Examples section, download the one you like the most, and start editing to see what happens.
๐ Table of Contents
Basic setup
Create an HTML document
<!DOCTYPE html>
<html>
<head>
<title>My First SmarkForm Form</title>
</head>
<body>
<h1>My First SmarkForm Form</h1>
</body>
</html>
Alternatively, you can just pick our Boilerplate file so that you can miss the third and fourth steps of this guide too and ๐ go stright to the nitty-gritty.
Include SmarkForm Library
Next, load SmarkForm to your document.
The easiest way is using a CDN:
<script src="https://cdn.jsdelivr.net/npm/smarkform@0.8.6/dist/SmarkForm.umd.js"></script>
Alternatively you can use a local copy of SmarkForm if you donโt want to rely on external sources like a CDN.
๐ You can get and include SmarkForm in your code in a lot of different ways.
You can also add an additional <script>
tag for the few JS code we will need to write.
Our complete layout may look as follows:
<!DOCTYPE html>
<html>
<head>
<title>My First SmarkForm Form</title>
<script src="https://cdn.jsdelivr.net/npm/smarkform@0.8.6/dist/SmarkForm.umd.js"></script>
</head>
<body>
<h1>My First SmarkForm Form</h1>
<!-- Your form markup goes here -->
<script>
/* Your JS code goes here */
</script>
</body>
</html>
Create a simple HTML form
Start by writing the form markup in plain HTML. For example, letโs create a simple form like the following:
<div id="myForm">
<p>
<label for="nameField">Name:</label>
<input type="text" id="nameField" name="name">
</p>
<p>
<label for="emailField">Email:</label>
<input type="email" id="emailField" name="email">
</p>
<p>
<button>โ Clear</button>
<button>๐พ Submit</button>
</p>
</div>
You may notice that there is no
<form>
tag. Just a regular<div>
, in this example with an Id that we will use to capture the node to enhance it as a SmarkForm instance.You can use a
<form>
tag instead if you like, but it is not actually necessary.
Initialize the Form
In your JavaScript tag, create a new instance of the SmarkForm class and pass the DOM node of the form container as parameter:
<div id="myForm">
<p>
<label for="nameField">Name:</label>
<input type="text" id="nameField" name="name">
</p>
<p>
<label for="emailField">Email:</label>
<input type="email" id="emailField" name="email">
</p>
<p>
<button>โ Clear</button>
<button>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
Ok: Nothing exciting happended by now.
๐ But keep readingโฆ ๐
Do the magic
By default, SmarkForm ignores all DOM elements in its container unless they are marked with the data-smark attribute.
This is because you can have html fields or buttons that belong to other components or functionalities of the page and you donโt want them to be taken as part of the form.
Letโs mark all fields, buttons and labelsโฆ by adding a data-smark attribute :
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input type="text" name="name" data-smark>
</p>
<p>
<label data-smark>Email:</label>
<input type="email" name="email" data-smark>
</p>
<p>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
Now, if you go to the Preview tab and fill some data in, you can then hit de โ Clear
button and see that, it already works!! ๐
Okay, we actually assigned a specific value to the Clear buttonโs data-smark attribute: {"action":"clear"}
. But thatโs all we did!! (We will get back to this in the next section).
Also notice that the for attribute of all <label>
s had been removed and they still work (if you click on them, corresponging fields get focus anyway).
All elements with a data-smark attribute are SmarkForm components of a certain type.
๐ SmarkForm componentsโ type is often implicit, either by their tag name (like the <label>
elements), their type attribute in case of <input>
s or by the presence of the action property that tell us they are action triggers.
Actions and Triggers
SmarkForm components with the action property defined in their data-smark object, are called โtriggersโ and they serve to invoke so called โactionsโ typically when the trigger is clicked or pressed.
For example, the โ Clear
button has the action property set to "clear"
, so when it is clicked, it will trigger the clear action.
However actions must be applied to some component. That component is called the context of the action.
The context of actions called by triggers is, by default, their natural context unless other component is specified through the context property.
The natural context of a trigger is the innermost of its SmarkForm component ancestors that implements that action (Which is the whole form in the previous example since the form component type implements the clear action).
๐ In fact, the clear action is implemented by all SmarkForm component types.
๐ Thatโs why the โ Clear
button cleared the whole form in the previous example.
The context of a trigger can be specified either as a relative path from its natural conext or as an absolute path from the form root.
Letโs add more โ Clear
buttons to clear each specific field:
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input type="text" name="name" data-smark>
<button data-smark='{"action":"clear", "context":"name"}'>โ Clear</button>
</p>
<p>
<label data-smark>Email:</label>
<input type="email" name="email" data-smark>
<button data-smark='{"action":"clear", "context":"email"}'>โ Clear</button>
</p>
<p>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
Since we cannot insert the trigger buttons inside the <input>
fields (unless using a singleton) we had to explicitly set the context of those triggers by the relative path from their natural context.
๐ Now you can either clear the whole form by clicking the โ Clear
button or just clear each field individually by clicking the โ Clear
button next to each field.
Exporting data
Although it may seem the opposite, the ๐พ Submit
button is already working. Itโs just we configured it to trigger the export action but we havenโt told it what to do with exported data.
๐ This can be done by listening to the AfterAction_export event.
The export of the form comes as JSON in the data property of the event object.
Example:
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input type="text" name="name" data-smark>
</p>
<p>
<label data-smark>Email:</label>
<input type="email" name="email" data-smark>
</p>
<p>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
/* Show exported data in an alert() window */
myForm.on("AfterAction_export", (event)=>{
window.alert(JSON.stringify(event.data, null, 4));
});
๐ Alternatively, most event handlers can be provided at once through the options object.
๐ Now go to the Preview tab, fill some data in and try clicking the ๐พ Submit
button.
Everything works now. ๐
All SmarkForm actions are just special methods that can be called from properly placed trigger components avoiding all the burden of manually wiring user interactions.
This enormously simplifies form controllersโ implementation, but they can also be be programatically invoked whenever required.
const {data} = await myForm.export();
Importing data
To load (JSON) data into the form, we use the import action which works in a similar way to the export action but in the opposite direction.
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input type="text" name="name" data-smark>
</p>
<p>
<label data-smark>Email:</label>
<input type="email" name="email" data-smark>
</p>
<p>
<button data-smark='{"action":"import"}'>๐ Import</button>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
/* Show exported data in an alert() window */
myForm.on("AfterAction_export", (event)=>{
window.alert(JSON.stringify(event.data, null, 4));
});
/* Import data from prompt() window */
myForm.on("BeforeAction_import", async (ev)=>{
/* Read new value: */
const json_template = '{"name": "", "email": ""}'; /* Little help to edit */
let data = window.prompt("Edit JSON data", json_template);
if (data === null) return void ev.preventDefault(); /* User cancelled */
/* Parse as JSON, warn if invalid and set */
try {
data = JSON.parse(data); ev.data = data; /* โ Set the new value */
} catch(err) {
alert(err.message); /* โ Show error message */
ev.preventDefault();
};
});
Form traversing
As we already stated, actions can be triggered both by interacting with trigger components or by calling them programatically like in the following example we already saw before:
const {data} = await myForm.export();
And the SmarkForm root form is just a field of the type โformโ.
What if we want to interact directly with a specific part of the form?
Here the .find()
method of every SmarkForm field comes to the rescue:
It takes a single argument with a โpath-likeโ route to the field we want to access.
This path can be either relative (to the current field) or absolute (to the form root).
<div id='myForm'>
<p>
<label for='id'>Id:</label>
<input data-smark type='text' name='id' />
</p>
<fieldset data-smark='{"type":"form","name":"personalData"}'>
<legend>Personal Data:</legend>
<p>
<label for='name'>Name:</label>
<input data-smark type='text' name='name' placheolder='Family name'/>
</p>
<p>
<label for='surname'>Surname:</label>
<input data-smark type='text' name='surname' />
</p>
<p>
<label for='address'>Address:</label>
<input data-smark type='text' name='address' />
</p>
</fieldset>
<fieldset data-smark='{"type":"form","name":"businessData"}'>
<legend>Business Data:</legend>
<p>
<label for='name'>Company Name:</label>
<input data-smark type='text' name='name' placheolder='Company Name'/>
</p>
<p>
<label for='address'>Address:</label>
<input data-smark type='text' name='address' />
</p>
</fieldset>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
/* Set business name */
myForm.onRendered(async ()=>{
myForm.find("/businessData").import({data: {name: "Bitifet"}});
/* ๐ Since we don't provide the address field, it will be cleared */
});
Context and Target
As weโve seen, when we trigger an action, we (implicitly or explicitly) provide a context to it.
๐ The context of an action is the component over which the action will be applied.
โ When called from a trigger component, the context is determined by the property of the same name in the data-smark object of the trigger if provided, or by the natural context of the trigger otherwise.
โ When called programatically, the context is simply the component over which the action method is called.
๐ In addition to the context many actions also accept another parameter called โtargetโ.
โ The target is the contextโs child over which the action will be applied.
โ โฆit is determined in a similar way to the context: If the direct parent of the trigger component does not implement the action (and hence cannot be the context), then the greandparent is checked in turn and, if so, the direct parent is picked as the target. And so on until the context is found.
This may seem confusing at first but, as you get used to it, you get the superpower of connecting actions to their context and target just by placing them in the propper place in the DOM tree.
๐ And, whenever it is not possible, you only need to specify the relative (or absolute) path in the context and/or target properties of the data-smark object of the trigger component..
Example 1: Lists
For instance, the addItem and removeItem actions are implemented only by the list component type so, if you put a removeItem trigger inside a list item, it will remove that item from the list. No wiring code needed!
<div id="myForm">
<p>
<label data-smark>Phones:</label>
<ul data-smark='{"name": "phones", "of": "input"}'>
<li>
<label data-smark>๐ </label>
<input placeholder='+34...' type="tel" data-smark>
<button data-smark='{"action":"removeItem"}' title='Remove Phone'>โ</button>
</li>
</ul>
<button data-smark='{"action":"addItem","context":"phones"}' title='Add Phone'>โ </button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
๐ This example uses the singleton pattern which is out of the scope of this section. But, by now, you can just think of the list items as SmarkForm field components of the type specified in the of property of the list component.
In the case of the addItem action, it will add a new item to the list after the SmarkForm field in which is placed (or before if the position option is set to โbeforeโ).
I wonโt repeat the previous example now, but you will find several examples of this across this manual.
Example 2: Import and Export targets
Even all SmarkForm field compoenent types implement both the export and import actions, so that the target will always default to null, we can explicitly set the target in import and/or export triggers:
๐ When the import action is called with an explicit target, it will call the export action of the target and import the data to its context.
๐ Similarly, when the export action is called with an explicit target, it will call the import action of the target with the exported data.
๐ This means we can clone list items or import/export whole subforms into other fields (no matter their type -since they will do their best to import received data-) with no single line of JS code like in the following example:
<div id="myForm">
<div style="display: flex; flex-direction:column; align-items:left; gap: 1em">
<div data-smark='{"name":"demo"}' style="flex-grow: 1">
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input type="text" name="name" data-smark>
</p>
<p>
<label data-smark>Email:</label>
<input type="email" name="email" data-smark>
</p>
</div>
</div>
<div style="display: flex; justify-content: space-evenly">
<span><button
data-smark='{"action":"export","context":"demo","target":"../editor"}'
title="Export 'demo' subform to 'editor' textarea"
>โฌ๏ธ Export</button></span>
<span><button
data-smark='{"action":"import","context":"demo","target":"../editor"}'
title="Import 'editor' textarea contents to 'demo' subform"
>โฌ๏ธ Import</button></span>
<span><button
data-smark='{"action":"clear", "context":"demo"}'
title="Clear the whole form"
>โ Clear</button></span>
</div>
<textarea
cols="20"
placeholder="JSON data viewer / editor"
data-smark='{"name":"editor","type":"input"}'
style="resize: vertical; align-self: stretch; min-height: 8em; flex-grow: 1;"
></textarea>
</div>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
โก Check the JS tab to see there is no import/export JS code at all and the preview tab to see how it works perfectly.
This trick is used in almost all the examples in the rest of this manual to provide the export/edit/import facilities except for that, unlike here, the surrounding layout is hidden for the sake of clarity.
Go furtherโฆ
However, if it were a larger form, we might not feel so comfortable with the โ Clear
button (โclearโ action trigger) clearing everything in case of an accidental click.
Luckily, we can listen to the BeforeAction_clear event and gently ask users for confirmation before they lose all their work.
Letโs see a simple example using a window.confirm() dialog:
<div id="myForm">
<p>
<label data-smark>Name:</label>
<input type="text" name="name" data-smark>
</p>
<p>
<label data-smark>Email:</label>
<input type="email" name="email" data-smark>
</p>
<p>
<button data-smark='{"action":"import"}'>๐ Import</button>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
/* Show exported data in an alert() window */
myForm.on("AfterAction_export", (event)=>{
window.alert(JSON.stringify(event.data, null, 4));
});
/* Import data from prompt() window */
myForm.on("BeforeAction_import", async (ev)=>{
/* Read new value: */
const json_template = '{"name": "", "email": ""}'; /* Little help to edit */
let data = window.prompt("Edit JSON data", json_template);
if (data === null) return void ev.preventDefault(); /* User cancelled */
/* Parse as JSON, warn if invalid and set */
try {
data = JSON.parse(data); ev.data = data; /* โ Set the new value */
} catch(err) {
alert(err.message); /* โ Show error message */
ev.preventDefault();
};
});
/* Ask for confirmation unless form is already empty: */
myForm.on("BeforeAction_clear", async ({context, preventDefault}) => {
if (
! await context.isEmpty() /* Form is not empty */
&& ! confirm("Are you sure?") /* User clicked the "Cancel" btn. */
) {
/* Prevent default (clear form) behaviour: */
preventDefault();
};
});
๐ Notice that now, if you go to the Preview tab and click the โ Clear
button before introducing any data nothing seems to happen (since the form is already empty).
๐ But, if you fill some data in and then click again, it will effectively ask before clearing the data.
Customize your form
Now you have a SmarkForm-enhanced form. SmarkForm will automatically handle form submission, validation, and other interactions based on the provided markup and configuration.
๐ You can customize the behavior and appearance of your SmarkForm form by configuring options and adding event listeners. SmarkForm provides a wide range of features and capabilities to simplify form development and enhance user experience.
๐ Start exploring the Core Concepts section for a deeper understanding of what is going on and continue with the rest of this SmarkForm manual for more details and examples to discover all the possibilities and unleash the power of markup-driven form development.
๐ Here you have a few examples of a larger forms that demonstrates some additional capabilities of the library:
List of telephones:
<div id="myForm">
<div data-smark='{"name":"phones","type":"list","of":"input","max_items":6,"sortable":true}'>
<div>
<button data-smark='{"action":"removeItem","hotkey":"-"}' title='Remove this item'><span role='img' aria-label='Remove this item'>โ</span></button>
<input data-smark='data-smark' type='tel' placeholder='Telephone'/>
<button data-smark='{"action":"addItem","hotkey":"+"}' title='Add new item below'><span role='img' aria-label='Add new item'>โ</span></button>
</div>
</div>
<p>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
/* Emphasize disabled buttons */
button:disabled {
opacity: .5;
}
/* Reveal hotkey hints */
button[data-hotkey] {
position: relative;
overflow-x: display;
}
button[data-hotkey]::before {
content: attr(data-hotkey);
display: inline-block;
position: absolute;
top: 2px;
left: 2px;
z-index: 10;
background-color: #ffd;
outline: 1px solid lightyellow;
padding: 1px 4px;
border-radius: 4px;
font-weight: bold;
transform: scale(1.4) translate(.1em, .1em);
}
const myForm = new SmarkForm(document.getElementById("myForm"));
/* Show exported data in an alert() window */
myForm.on("AfterAction_export", (event)=>{
window.alert(JSON.stringify(event.data, null, 4));
});
/* Import data from prompt() window */
myForm.on("BeforeAction_import", async (ev)=>{
/* Read new value: */
const json_template = '{"name": "", "email": ""}'; /* Little help to edit */
let data = window.prompt("Edit JSON data", json_template);
if (data === null) return void ev.preventDefault(); /* User cancelled */
/* Parse as JSON, warn if invalid and set */
try {
data = JSON.parse(data); ev.data = data; /* โ Set the new value */
} catch(err) {
alert(err.message); /* โ Show error message */
ev.preventDefault();
};
});
/* Ask for confirmation unless form is already empty: */
myForm.on("BeforeAction_clear", async ({context, preventDefault}) => {
if (
! await context.isEmpty() /* Form is not empty */
&& ! confirm("Are you sure?") /* User clicked the "Cancel" btn. */
) {
/* Prevent default (clear form) behaviour: */
preventDefault();
};
});
๐ Limited to a maximum of 6 numbers.
๐ Notice trigger butons get properly disabled when limits reached.
๐ They are also excluded from keyboard navigation for better navigation with tab
/ shift
+tab
.
๐ Even thought they can be triggered from keyboard through configured (Ctrl
-+
and Ctrl-
-`) hot keys.
๐ โฆNotice what happen when you hit the ctrl
key while editingโฆ
๐ Also note that, when you click the ๐พ Submit
button only non empty items get exported.
๐ โฆYou can prevent this behaviour by setting the exportEmpties property to true.
๐ Change items order by just dragging and dropping them.
Simple schedule:
<div id="myForm">
<p>
<button data-smark='{"action":"removeItem","hotkey":"-","context":"surveillance_schedule"}' title='Less intervals'>
<span role='img' aria-label='Remove interval'>โ</span>
</button>
<button data-smark='{"action":"addItem","hotkey":"+","context":"surveillance_schedule"}' title='More intrevals'>
<span role='img' aria-label='Add new interval'>โ</span>
</button>
<label>Schedule:</label>
<span data-smark='{"type":"list","name":"surveillance_schedule","min_items":0,"max_items":3,"exportEmpties":true}'>
<span>
<input data-smark type='time' name='start'/>
to
<input data-smark type='time' name='end'/>
</span>
<span data-smark='{"role":"empty_list"}'>(Closed)</span>
</span>
</p>
<p>
<button data-smark='{"action":"clear"}'>โ Clear</button>
<button data-smark='{"action":"export"}'>๐พ Submit</button>
</p>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
/* Show exported data in an alert() window */
myForm.on("AfterAction_export", (event)=>{
window.alert(JSON.stringify(event.data, null, 4));
});
/* Import data from prompt() window */
myForm.on("BeforeAction_import", async (ev)=>{
/* Read new value: */
const json_template = '{"name": "", "email": ""}'; /* Little help to edit */
let data = window.prompt("Edit JSON data", json_template);
if (data === null) return void ev.preventDefault(); /* User cancelled */
/* Parse as JSON, warn if invalid and set */
try {
data = JSON.parse(data); ev.data = data; /* โ Set the new value */
} catch(err) {
alert(err.message); /* โ Show error message */
ev.preventDefault();
};
});
/* Ask for confirmation unless form is already empty: */
myForm.on("BeforeAction_clear", async ({context, preventDefault}) => {
if (
! await context.isEmpty() /* Form is not empty */
&& ! confirm("Are you sure?") /* User clicked the "Cancel" btn. */
) {
/* Prevent default (clear form) behaviour: */
preventDefault();
};
});
to (Closed)
Now that you understand the basics maybe a good opportunity to revisit the Showcase section and examine the source code of those examples.
๐ Also donโt miss the Examples section for more complete and realistic use casesโฆ
Final notes
Boilerplate file
If you prefer to start from scratch, congratulations!!
But, be aware that SmarkForm is intended to not interfere with your HTML and CSS.
It adds great functionality but does nothing with the layout and styling of the page or even its components.
Nevertheless, if you liked the styling of our examples and want to start a new project, donโt miss our Boilerplate Template.
You can pick it from the Resources โก๏ธ Download section of this manual.
You donโt need a form tag
You donโt even need a <form>
tag. In fact it is not (yet) advised to use it for SmarkForm forms.
If you do so, they will be submit-prevented so they can act as kind of failback behvaviours in case of JavaScript being disabled.
But itโs not yet clear which could be a future enhancenment of native <form>
attributes, such as action, in successfully enhanced <form>
tags.
Using your own copy of SmarkForm library
If you donโt want to rely on an external resource like a CDN, take the following steps:
- Download โคต๏ธ latest SmarkForm version.
- Place it beside your HTML file.
- And use following code instead to linki it in your HTML file:
<script src="path/to/SmarkForm.js"></script>
Alternative forms to get/include Smarkform
There is plenty of ways to get SmarkForm. Either as ESM module or highly compatible UMD format you can download it from this page, install as npm package, directly link as a CDNโฆ
For more detailed information, check out the section Getting SmarkForm.
The optionโs object
If you want a more compact syntax to provide, at least, most basic functionality at once, you can use the options object.
const myForm = new SmarkForm(
document.getElementById("myForm")
, {
onAfterAction_export({data}) {
// Show exported data:
console.log(data);
},
}
);
All AfterAction_xxx and BeforeAction_xxx events can get their first event handler attached through functions respectively named onAfterAction_xxx and onBeforeAction_xxx in the options object passed to the SmarkForm constructor.
The only differrence here is that AfterAction_xxx and BeforeAction_xxx events can be locally attached to any SmarkForm field.
Handlers passed through that options are attached to the root form.