Forms

Input masking

Input masking

Laravolt v7 ships a thin adapter around Inputmask. A ->mask(...) call on any text-like field produces the data-inputmask options Inputmask expects, along with sensible inputmode attributes for mobile keyboards.

Why masks, not regex

Input masks guide users while they type. A phone mask lets a user see the final format as they key it in; a currency mask adds grouping separators; a date mask refuses letters and keeps the shape of yyyy-mm-dd. This is a better UX than "submit and get an error".

Masks complement — they do not replace — server-side validation. The mask is polish; the FormRequest is the contract.

Quick start

Blade
{!! PrelineForm::text('phone')->mask('phone') !!}
{!! PrelineForm::text('amount')->mask('currency') !!}
{!! PrelineForm::text('birthday')->mask('date') !!}

Each call sets two data attributes on the rendered <input>:

HTML
<input type="text" name="phone"
data-mask="phone"
data-inputmask='{"mask":"(+99) 9999-9999[9]","placeholder":"_"}'
inputmode="tel">

Initialize Inputmask on page load (see Initializing Inputmask) and every masked field is live.

Available presets

PresetBehavior
phoneInternational-friendly mask (+99) 9999-9999[9]. Adds inputmode="tel".
currencyInputmask currency alias. Default prefix Rp , . as group separator, , as radix point, 0 decimals. Submits the unmasked number.
datedatetime alias with inputFormat="yyyy-mm-dd".
datetimedatetime alias with inputFormat="yyyy-mm-dd HH:MM".
timedatetime alias with inputFormat="HH:MM".
emailInputmask email alias.

Override options on any preset by passing a second argument:

PHP
PrelineForm::text('birthday')->mask('date', [
'inputFormat' => 'dd/mm/yyyy',
'placeholder' => 'dd/mm/yyyy',
]);

The override merges on top of the preset definition, so you only supply what you want to change.

Custom masks

Any string other than a preset name is treated as a raw Inputmask pattern:

PHP
PrelineForm::text('sku')->mask('AAA-9999');
PrelineForm::text('ip')->mask('999.999.999.999');
PrelineForm::text('card')->mask('9999 9999 9999 9999');

Inputmask's mask characters apply:

CharMeaning
9Numeric
aAlphabetical
AAlphabetical uppercase
*Any
[...]Optional group

See the Inputmask documentation for the full specification.

Raw options

When a preset is not expressive enough, pass a raw options array with ->mask([...]) or ->inputmask([...]). Both forms behave identically — inputmask exists for readability.

PHP
PrelineForm::text('code')->inputmask([
'mask' => '999-AAA',
'casing' => 'upper',
'placeholder' => '_',
]);
PrelineForm::text('uk_postcode')->mask([
'mask' => '[a]AA 9[a][a]',
'greedy' => false,
'casing' => 'upper',
]);

Removing a mask

Use ->unmask() to strip mask attributes from a field. Handy for conditionally-masked inputs:

PHP
$input = PrelineForm::text('identifier')->mask('AAA-9999');
if ($team->usesFreeFormIds()) {
$input->unmask();
}

Initializing Inputmask

Laravolt does not bundle the Inputmask JavaScript itself; it only emits the data attributes. Include Inputmask via npm, CDN, or your preferred asset pipeline, then initialize every field that has data-inputmask:

HTML
<!-- CDN -->
<script src="https://cdn.jsdelivr.net/npm/inputmask@5/dist/inputmask.min.js"></script>
<script>
document.querySelectorAll('[data-inputmask]').forEach((element) => {
Inputmask(JSON.parse(element.dataset.inputmask)).mask(element);
});
</script>

Or from an application JavaScript entry:

JavaScript
import Inputmask from 'inputmask';
function initMasks(root = document) {
root.querySelectorAll('[data-inputmask]').forEach((element) => {
const options = JSON.parse(element.dataset.inputmask);
Inputmask(options).mask(element);
});
}
initMasks();
// Re-run after Livewire updates swap DOM nodes in.
document.addEventListener('livewire:init', () => {
Livewire.hook('morph.added', ({ el }) => initMasks(el));
});

Livewire and dynamic markup

Re-initialize masks after any DOM swap. Livewire's morph.added hook, Alpine's x-init, or a simple MutationObserver on your main content area all work. Without this, fields loaded after the initial page render will not be masked.

Submitting masked values

Some masks (like currency) submit the unmasked value by default, because removeMaskOnSubmit and autoUnmask are enabled in the preset. Others submit the formatted value as typed. Decide per form:

  • Store numbers as numbers. Currency, phone counts — keep them unmasked on submit so the server parses cleanly.
  • Store formatted values only when the format is part of the business meaning. For example if your domain treats "ABC-0001" as the canonical identifier, keep it masked.

Either way, document the decision in the corresponding FormRequest rule and test it.

Accessibility notes

  • Keep a clear label on every masked input. Screen readers depend on it.
  • Use hint() to describe the expected format in plain language ("Format: DD/MM/YYYY").
  • Prefer <label> text that mirrors the mask intent ("Phone number"), not the mask pattern itself.
  • Inputmask already forwards keystrokes to assistive tech; do not block paste events or add custom beforeinput handlers that fight the library.

Testing masks

Masks are a frontend concern, so unit tests focus on the emitted attributes rather than the visual behaviour:

PHP
it('applies the phone preset', function () {
$html = PrelineForm::text('phone')->mask('phone')->render();
expect($html)
->toContain('data-mask="phone"')
->toContain('data-inputmask=')
->toContain('inputmode="tel"');
});

For the browser-level UX, rely on Playwright/Dusk/integration tests that submit the form and assert the server receives the expected value.

  • Validation — the server-side contract that masks decorate.
  • Forms overview — the full form builder API.
  • AI-ready platform — why keeping masks declarative and testable matters when AI tools modify forms.
Previous
Validation