Field Masking
π Table of Contents
SmarkFormβs masking API lets you integrate any external input-masking library β or a pure-JavaScript custom mask β to format user input while keeping exported data clean and unformatted. SmarkForm does not reinvent input masking; instead it provides a thin declarative layer that connects external masking solutions to SmarkForm-managed fields.
Masks are defined with SmarkForm.registerMask() (JavaScript) or <script type="smark-mask"> (declarative HTML), and applied to fields via the mask property in data-smark.
How It Works
When a field has a mask property in its data-smark, SmarkForm:
- Saves the original input type (e.g.
number,tel). - Converts the input type to
textso masking libraries can operate freely. - Looks up the mask factory by name β first in scoped (mixin) masks, then in the global registry.
- Awaits the factory (factories may be
async). The factory receives the fieldβs target DOM node and must return an object with anunmaskedValueproperty (getter/setter pair) β ornull/undefinedfor unmasked fields. - Restores focus to its previous owner if the factory stole it (some third-party mask libraries focus the field as a side effect).
- Stores the returned mask instance in
_maskInstance. - Exports the unmasked value (raw data) instead of the formatted display.
- Dispatches
inputevents when values are set programmatically so masks stay synchronized. - If the mask fails (not found or factory throws), the original type is restored and the field operates unmasked.
Applying a Mask to a Field
Add the mask property to the fieldβs data-smark. The mask is applied automatically when the field renders β no post-construction setup needed.
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<div id="myForm">
<label data-smark>Card Number:</label>
<input
data-smark='{"type":"number","name":"card","mask":"cardNumber"}'
placeholder="0000 0000 0000 0000"
>
</div>
SmarkForm.registerMask("cardNumber", (node) => {
const imask = new IMask(node, { mask: "0000 0000 0000 0000" });
return imask;
});
const myForm = new SmarkForm(document.getElementById("myForm"));
The HTML declares a number-type field with mask: "cardNumber". SmarkForm converts it to type="text" so IMask can operate, then exports the clean digit string.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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 mask property should point to a previously registered mask factory.
- Mask factories can rely on external libraries (e.g. IMask) or be custom JavaScript implementations.
- The only requirement is that they return an object with an
unmaskedValueproperty (getter/setter pair).- The former example uses IMask for basic credit card formatting.
- Since the field type is converted to
text, native enhancements, default inputMode, etc⦠are lost. The factory is responsible for restoring any desired behavior (e.g.inputMode: "numeric"for mobile keyboards).
Registering a Mask
Via JavaScript
Call SmarkForm.registerMask() before constructing any form that uses the mask. The factory receives the fieldβs target DOM node and must return an object with an unmaskedValue property (getter/setter pair) so SmarkForm can read and write the clean value independently of the formatted display.
Factories may be async β SmarkForm awaits them. If a mask library performs deferred work (e.g. focusing the field via setTimeout), the factory should await that work before returning so that SmarkForm can restore focus to its previous owner. See Focus and Mask Factories for details.
Returning null or undefined from the factory is allowed: the field operates unmasked. This is useful for conditional masking.
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<div id="myForm">
<label data-smark>Card Number:</label>
<input
data-smark='{"type":"number","name":"card","mask":"cardNumber"}'
placeholder="0000 0000 0000 0000"
>
</div>
SmarkForm.registerMask("cardNumber", (node) => {
return new IMask(node, { mask: "0000 0000 0000 0000" });
});
const myForm = new SmarkForm(document.getElementById("myForm"));
The factory receives the DOM node after SmarkForm has already converted its type from number to text. Any library or custom code that operates on the node will work β SmarkForm only cares about the returned objectβs unmaskedValue property.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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.
Via Declarative HTML
Place a <script type="smark-mask" data-name="..."> element anywhere in the page. SmarkForm scans for these on construction and registers each factory automatically. The fieldβs mask property in data-smark tells it which factory to use β no JavaScript needed beyond the constructor.
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<script type="smark-mask" data-name="cardNumber">
(node) => {
return new IMask(node, { mask: "0000 0000 0000 0000" });
}
</script>
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<div id="myForm">
<label data-smark>Card Number:</label>
<input
data-smark='{"type":"number","name":"card","mask":"cardNumber"}'
placeholder="0000 0000 0000 0000"
>
</div>
const myForm = new SmarkForm(document.getElementById("myForm"));
Everything is defined in the HTML: the smark-mask script element registers the factory (SmarkForm scans the document for these during construction), and the inputβs data-smark references it by name via the mask property. No registerMask() call is needed.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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.
Script elements inside a <template> are treated as mixin-scoped masks (see Mixin-Scoped Masks).
Credit Card Example (IMask)
Building on the basic examples above, this fully-worked credit card field adds several production-quality refinements:
- The placeholder and mobile keyboard hint are set inside the factory so the HTML stays clean (DRY).
- IMask starts lazy so the native placeholder shows when the field is empty. After the first digit, it switches to a non-lazy mode with underscore padding for unfilled positions, keeping the cursor in the right place.
- The factory returns a wrapper object whose
unmaskedValuegetter returnsnullwhenisCompleteis false β incomplete card numbers are never included inexport().
<script src="https://cdn.jsdelivr.net/npm/imask@6.6.3"></script>
<div id="myForm">
<div data-smark='{"type":"form","name":"payment"}'>
<p>
<label data-smark>Card Number:</label>
<input data-smark='{"type":"number","name":"cardNumber","mask":"card"}'>
</p>
</div>
</div>
input:invalid {
outline: 1px solid #d4c070;
outline-offset: -1px;
}
SmarkForm.registerMask("card", (node) => {
// DRY: set placeholder and keyboard hint inside the factory
node.placeholder = "0000 0000 0000 0000";
node.inputMode = "numeric";
// Start lazy so the native placeholder shows while empty
let showLazy = true;
const imask = new IMask(node, {
mask: "0000 0000 0000 0000",
lazy: true,
});
let prevValue = node.value;
let lastKey = "";
node.addEventListener("keydown", (e) => { lastKey = e.key; });
node.addEventListener("input", () => {
const hasContent = imask.masked.unmaskedValue.length > 0;
if (hasContent && showLazy) {
// First digit: switch to non-lazy + underscore placeholders
showLazy = false;
imask.updateOptions({
mask: "0000 0000 0000 0000",
lazy: false,
placeholderChar: "_",
});
} else if (!hasContent && !showLazy) {
// Last digit deleted: switch back to lazy (. .restore placeholder)
showLazy = true;
imask.updateOptions({
mask: "0000 0000 0000 0000",
lazy: true,
});
}
// Blink on rejected input, but not for Backspace/Delete hitting a separator
if (node.value === prevValue && node === document.activeElement) {
if (lastKey !== "Backspace" && lastKey !== "Delete") {
node.style.boxShadow = "0 0 0 2px #f80";
setTimeout(() => node.style.boxShadow = "", 250);
}
}
prevValue = node.value;
// Mark invalid when partially filled (triggers native :invalid CSS)
const raw = imask.masked.unmaskedValue;
const showError = raw.length > 0 && !imask.masked.isComplete;
node.setCustomValidity(showError ? "Please enter the full 16-digit card number" : "");
});
// Wrap IMask so unmaskedValue returns null for incomplete cards
return {
get unmaskedValue() {
return imask.masked.isComplete ? imask.masked.unmaskedValue : null;
},
set unmaskedValue(v) {
// Use IMask's top-level setter (syncs the DOM via updateControl).
imask.unmaskedValue = v;
// IMask diffs an `input` event against its last saved selection, which
// is only refreshed on focus/keydown. Since SmarkForm dispatches an
// `input` event after every import, a stale selection would make IMask
// read the imported value as a duplicate insertion (e.g. "4111" β
// "4111 4111"). Sync caret and selection to the end instead.
const len = node.value.length;
node.setSelectionRange(len, len);
imask._selection = { start: len, end: len };
},
};
});
const myForm = new SmarkForm(document.getElementById("myForm"));
The wrapper object overriding unmaskedValue to return null for incomplete numbers is the key refinement β export() never returns partially-typed card data. The field is marked :invalid when partially filled (not empty but not complete), and rejected keystrokes trigger a brief orange blink β visually distinct from the persistent invalid state.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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.
Custom Mask Example (No Library + Singleton + List)
You donβt need a third-party masking library. A plain JavaScript object with unmaskedValue getter/setter is sufficient. This example strips non-digit characters on export β a simple βdigits onlyβ mask.
The example also demonstrates how masks work inside list items wrapped in a singleton. Each phone <input> sits inside a <span data-smark='{"type":"input"β¦}'> that SmarkForm treats as a standalone singleton field. The mask property placed on the singleton wrapper is automatically inherited by the inner <input>, and the listβs add button lets you add or remove phones dynamically.
<div id="myForm">
<div data-smark='{"type":"form","name":"contacts"}'>
<p>
<label>Contact Phones</label>
<div data-smark='{"type":"list","name":"phones"}'>
<p style="display:flex;gap:8px;margin:4px 0;align-items:center">
<span data-smark='{"type":"input","name":"phone","mask":"digits"}'>
<input data-smark type="tel" placeholder="Phone number">
</span>
<button data-smark='{"action":"removeItem"}' title="Remove">β</button>
</p>
</div>
<button data-smark='{"action":"addItem","context":"phones"}'>Add Phone</button>
</p>
</div>
</div>
SmarkForm.registerMask("digits", (node) => {
node.inputMode = "numeric";
let _raw = '';
let prevValue = node.value;
node.addEventListener('input', () => {
const before = prevValue;
prevValue = node.value;
const digits = node.value.replace(/\D/g, '');
const formatted = digits.replace(/(\d{3})(?=\d)/g, '$1 ').trim();
if (formatted !== node.value) node.value = formatted;
_raw = digits;
// Blink if value was reverted (invalid character typed)
if (node.value === before && node === document.activeElement) {
node.style.boxShadow = "0 0 0 2px #f80";
setTimeout(() => node.style.boxShadow = "", 250);
}
});
return {
get unmaskedValue() { return _raw; },
set unmaskedValue(v) { _raw = v; node.value = v; },
};
});
const myForm = new SmarkForm(document.getElementById("myForm"));
Each phone input is wrapped in a singleton (type:"input") with mask:"digits" on the wrapper. The mask is inherited by the inner <input> automatically, and export() returns only the digits. Add new phones with the button β each new item inherits the mask from its singleton wrapper.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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.
Mixin-Scoped Masks
When a <script type="smark-mask"> is placed inside a <template> (used for mixin types), the mask is scoped to that mixinβs expansion β it does not register globally. This prevents name collisions between mixins that define masks with the same name.
A mixin-local mask overrides a global mask with the same name, so mixins can safely define their own versions of shared mask names.
See also: Mixin Types β Scripts and Styles for the full mixin script policy, including the
smark_mixin_allowLocalScriptsoption.
<div id="myForm">
<div data-smark='{"type":"#digitsMixin","name":"mixinField"}'></div>
</div>
<template id="digitsMixin">
<!-- Scoped mask: sibling of the root element inside the template -->
<script type="smark-mask" data-name="digits">
(node) => {
let _v = '';
node.addEventListener('input', () => {
_v = node.value.replace(/\D/g, '');
if (_v !== node.value) node.value = _v;
});
return { get unmaskedValue() { return _v; }, set unmaskedValue(v) { _v = v; node.value = v; } };
}
</script>
<div>
<input data-smark='{"name":"inner","mask":"digits"}' type="text" placeholder="Digits only (mixin-scoped)">
</div>
</template>
const myForm = new SmarkForm(document.getElementById("myForm"), {"smark_mixin_allowLocalScripts":"allow"});
The digits mask is defined inside the #digitsMixin template via a <script type="smark-mask"> element. SmarkForm scopes it to the mixinβs expansion β the mask is available to fields inside the mixin but does NOT appear in the global registry. This prevents naming conflicts between different mixins that define masks with the same name.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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.
This example requires smark_mixin_allowLocalScripts: "allow" because the <script> inside the <template> must be executed by the mixin system.
Using Other Masking Libraries (Maska)
SmarkForm works with any masking library β not just IMask. The only requirement is that the factory returns an object with an unmaskedValue getter/setter pair.
This example uses Maska to format a price field with thousand separators and two decimal places. The factory uses Maskaβs Mask class for synchronous formatting and listens to the maska event to track the unmasked value for export.
<script src="https://cdn.jsdelivr.net/npm/maska@1.5.1/dist/maska.js"></script>
<div id="myForm">
<p>
<label>Price:</label>
<input data-smark='{"type":"number","name":"price","mask":"price"}' placeholder="0.00">
</p>
</div>
SmarkForm.registerMask("price", (node) => {
node.inputMode = "decimal";
Maska.create(node, {
mask: "####.##",
tokens: {
"#": { pattern: /[0-9]/ },
},
});
return {
get unmaskedValue() {
const raw = node.dataset.maskRawValue || "";
if (!raw) return "";
// maskRawValue is just digits. For "####.##", last 2 are decimal:
const dotp = Math.min(4, raw.length);
return Number(raw.slice(0, dotp) + '.' + raw.slice(dotp, dotp+2));
},
set unmaskedValue(v) {
const num = Number(v);
if (
v === "" || v === null || v === undefined
|| isNaN(num)
|| num < 0 || num > 9999.99
) {
node.value = "";
node.dispatchEvent(new Event("input", {bubbles: true}));
return;
}
// Pre-format to exactly 4+2 raw digits matching "####.##":
const raw = Math.round(Math.abs(num) * 100);
node.value = String(Math.floor(raw / 100)).padStart(4, "0").slice(-4)
+ String(raw % 100).padStart(2, "0");
node.dispatchEvent(new Event("input", {bubbles: true}));
},
};
});
const myForm = new SmarkForm(document.getElementById("myForm"));
The factory creates a Maska instance with a ####.## pattern for four integer digits and two decimal places. Maska stores the raw digit string (without separators) in node.dataset.maskRawValue.
The getter parses that string, inserts a decimal point two positions from the right and returns a proper number (e.g. "123456" β 1234.56).
The setter pre-formats the incoming number to the exact 4+2 raw digits the mask expects (e.g. 1234.56 β "123456"), writes it to the field and dispatches an input event so Maska reformats the display. Values outside the 0..9999.99 range the pattern can represent are rejected and the field is cleared instead.
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.β»οΈ Resetin forms prefilled with sample data, resets 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 playground 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.
Error Handling
SmarkFormβs masking error handling is controlled by the smark_mask_throwOnMissing constructor option (default true):
smark_mask_throwOnMissing: true (default)
- Mask not found β throws
MASK_NOT_FOUNDrender error. - Mask factory throws β throws
MASK_APPLY_ERRORrender error (the original exception is available viaerror.cause). - In both cases the input type is restored to its original value before the error is raised, so the error indicator is shown with the correct type.
smark_mask_throwOnMissing: false
- Mask errors are reported via
console.warninstead of throwing. - The fieldβs original input type is restored and the field operates unmasked.
const myForm = new SmarkForm("#myForm", {
smark_mask_throwOnMissing: false
});
Error Codes
| Code | Meaning |
|---|---|
MASK_NOT_FOUND | No factory registered for the given mask name |
MASK_APPLY_ERROR | Factory was found but threw during execution; inspect error.cause |
See Error Codes Reference for all SmarkForm error codes.
Focus and Mask Factories
Some third-party mask libraries focus the input field as a side effect of initialization β typically via setTimeout(focus, 0). Since SmarkForm awaits the factory before restoring focus, any deferred focus calls must have fired before the factory returns.
If the library focuses the field synchronously (e.g. IMask, Maska), no special handling is needed β SmarkForm restores focus after the factory returns.
If the library focuses the field asynchronously (e.g. Inputmask via setTimeout), the factory should await a macrotask yield so that the deferred focus fires before returning:
SmarkForm.registerMask("price", async (node) => {
Inputmask({ alias: "numeric", ... }).mask(node);
// Yield one macrotask so Inputmask's deferred focus fires.
// SmarkForm will then restore focus to its previous owner.
await new Promise((r) => setTimeout(r, 0));
return { get unmaskedValue() { ... }, set unmaskedValue(v) { ... } };
});
Without this yield, Inputmask would focus the field after SmarkForm restores focus, leaving the field unexpectedly focused.
See the FAQ β My masked field gets focused unexpectedly for the full Inputmask case study, including the known limitation where Inputmask re-focuses the field after a programmatic blur().
Masks and External Libraries
SmarkForm does not include or prescribe any masking library. You are free to use:
- IMask.js β feature-rich, pattern-based masking (demonstrated in examples above).
- Maska β lightweight, zero-dependency masking for vanilla JS, Vue, Alpine.js, and Svelte (see Using Other Masking Libraries).
- A custom pure-JavaScript factory β see Custom Mask Example.
- Any other library that can be wrapped in a factory returning
{ unmaskedValue }. - Plain DOM event handlers β the factory can attach listeners and return a simple object.
The only contract is:
The factory receives one argument (the target
<input>element) and returns an object with anunmaskedValueproperty (getter/setter pair), ornull/undefinedto indicate βno maskingβ.The
unmaskedValuegetter may return any scalar type (string, number, boolean,null). SmarkForm stringifies the getterβs return value when exporting to non-null fields, so a getter that returns a number is fine β e.g.return 1234.56;. A returnednullis preserved as-is to signal βno valueβ (useful for partially-filled fields). The setter receives the raw unformatted value and is responsible for writing the formatted display.