wemersonrv/input-mask
A Laravel package that adds input masking to forms via Blade components, with ready-to-use masks for CPF/CNPJ, phone, date, currency, and custom patterns.
Read the source before the README. The package description says "Blade directives and helpers," which reads like a general Laravel form library. The repository is a Laravel Nova custom field: a PHP Field subclass plus a Vue component that renders a masked input inside Nova's resource forms. It does nothing in Blade, Livewire, or Inertia. Plan against src/ and resources/js/, not the marketing copy.
Verify three things before writing integration code:
2019-05-16, which lands in the Nova 1.x/2.x era (Vue 2). Nova 3 changed field internals; Nova 4/5 moved to Vue 3. On Nova 4+ assume the frontend is broken and the install step degrades to "port it."composer.json for the laravel/nova and PHP constraints, package.json for the bundled JS mask plugin (name, version, license), and the Field class for the exact namespace and fluent setter name — the snippet below is illustrative, not guaranteed.Minimal steps (on a branch, in a legacy Nova install):
composer require wemersonrv/input-mask
use Wemersonrv\InputMask\InputMask; // verify namespace in src/
public function fields(Request $request)
{
return [
InputMask::make('Phone')
->mask('(99) 99999-9999'), // verify setter name in the Field class
];
}
Then run php artisan nova:publish / npm run dev if the field ships unbuilt assets, load the resource form, and submit it and inspect the database row. This is the step people skip. You are verifying whether the field strips mask characters or persists (11) 99999-8888 verbatim — that answer drives every downstream decision about validation, search, and exports.
First use case: a single internal-admin field where operators type a Brazilian format (CPF, CNPJ, phone, postal code, date, currency). Not customer-facing forms, not anything feeding an export or an integration.
Where to look first: the src/ Field class (the entire PHP API — likely a make() plus one metadata setter), the Vue component in resources/js/ (the entire behavior), and the bundled mask library. The package is small enough that the source is the documentation; read all three before touching a resource.
Wrap it in your own field class. Don't sprinkle Wemersonrv\InputMask\InputMask::make() through twenty resources — the upstream name is your migration liability.
namespace App\Nova\Fields;
class MaskedText extends \Wemersonrv\InputMask\InputMask
{
public function phone(): static { return $this->mask('(99) 99999-9999'); }
public function document(): static { return $this->mask('999.999.999-99'); }
public function postalCode(): static { return $this->mask('99999-999'); }
}
When you swap the underlying component (or port to Vue 3), every call site stays untouched.
Treat the mask as presentation and put the real logic in the request layer. This is what makes the field swappable:
protected function prepareForValidation(): void
{
$this->merge([
'phone' => preg_replace('/\D+/', '', (string) $this->input('phone')),
'document' => preg_replace('/\D+/', '', (string) $this->input('document')),
]);
}
Normalize before validation so rules speak in canonical digits (->rules('digits:11'), not a regex that has to account for parentheses). Persist the normalized form; re-apply the mask only at render time. For currency, parse to integer minor units; for dates, Carbon::createFromFormat against the expected pattern with a fallback.
Pair every masked field with a display formatter. Stored 11999998888 is unreadable in index and detail views. Add a Nova Text field using ->displayUsing(fn ($v) => format_phone($v)), or a model accessor if the value is used outside Nova. Otherwise the mask's benefit exists only inside the edit form.
Decide one direction and hold it across the codebase. Either (a) store unmasked and format on display — best for search, joins, and exports; or (b) store masked and normalize on read — only if legacy columns already contain punctuation. Mixing shapes across resources is how you get "why doesn't search find this record."
Reference-implementation pattern (the realistic one for modern Nova). Read the two source files, then rebuild: copy the Vue component, replace the bundled mask plugin with a maintained one (IMask or cleave.js), bump to Vue 3, and publish it as a local Nova component in nova-components/. You get version control, an MIT-clean license, and a mask library you chose. This is a one-to-two-day job and usually cheaper than making the 2019 asset pipeline cooperate.
Spike matrix — test the exact tokens you intend to use, not just "does it render":
| Action | What to confirm |
|---|---|
| Type a full value | Characters land in the right slots |
| Paste a pre-formatted value | Not doubled or truncated |
| Paste a bare-digit value | Mask applies, no mangling |
| Clear the field | Emits empty, not placeholder debris |
| Submit empty | Server rule behaves |
| Browser autofill | Doesn't bypass or break the mask |
Server-side submit via curl |
Confirms the mask is not a validation layer |
That last row is the one that matters most: post the form directly, bypassing the UI, and confirm your validation rejects bad input. Mask libraries diverge sharply on paste and autofill.
curl bypasses the widget entirely. Every masked field needs a server-side rule and a normalization test. If a stakeholder frames this as "solving our data quality problem," correct that in the same meeting.LIKE/join/export against that column is wrong. Normalize at the FormRequest or model boundary rather than migrating the schema mid-project.resources/js/; the PHP class only passes metadata. Customizing means editing and rebuilding that component — and owning the rebuild forever. "Just patch it" is rarely the cheap option it looks like.$field->meta() inside fields() to confirm mask metadata reaches the frontend, then check the Nova browser console for a failed registration (Nova.booting) error. Nine times out of ten it's a Vue version mismatch or an unbundled asset, not the mask config.help text stating the expected format if your admin tooling has accessibility commitments.How can I help you explore Laravel packages today?