WakaMask

WakaMask is a live input masking plugin for wakaPAC. It formats phone numbers, dates, codes, and any other literal/placeholder pattern as the user types. There are no parallel DOM listeners — keystrokes, deletions, and paste are intercepted through wakaPAC's existing message pipeline (MSG_KEYDOWN, MSG_CHAR, MSG_PASTE), the same pipeline every other event in your component already flows through.

explanation

Getting Started

Include the script after wakaPAC, register the plugin, then add data-pac-mask to any text input:

<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakamask.min.js"></script>

<script>
    wakaPAC.use(WakaMask); // must be called before any wakaPAC() calls
</script>

data-pac-mask and data-pac-bind are independent — the mask controls formatting, the bind controls which abstraction property the (already formatted) value syncs to:

<div id="contact">
    <input type="text" data-pac-mask="999-999-9999" data-pac-bind="value: phone" placeholder="555-123-4567">
    <span data-pac-bind="visible: phoneStatus">{{ phoneStatus }}</span>
</div>
wakaPAC('#contact', {
    phone: '',
    phoneStatus: '',

    msgProc(event) {
        switch (event.message) {
            case WakaMask.MSG_MASK_REJECT:
                this.phoneStatus = `"${String.fromCharCode(event.wParam)}" isn't a digit`;
                break;

            case WakaMask.MSG_MASK_COMPLETE:
                this.phoneStatus = 'Looks good';
                break;
        }
    }
});

WakaMask scans a component's container for [data-pac-mask] inputs when the component is created and reformats the field as the user types. No other configuration is required — this.phone always holds the formatted display value, since that's what two-way binding reads off the input.

Usage

Pattern Tokens

A mask pattern is a sequence of tokens. Any character not listed below is a literal — inserted automatically and never directly typed over:

Token Matches
9 A single digit, [0-9].
a A single letter, [A-Za-z].
* A single alphanumeric character, [A-Za-z0-9].
\x Escapes x so a token character (9, a, *, or \) is treated as a literal instead of a slot.

Examples: 999-999-9999 (US phone), 99/99/9999 (date), aaa-999 (license-plate style), ***-*** (alphanumeric code).

Registering a Masked Field

Add data-pac-mask directly to an <input type="text">. Any pre-existing value (e.g. server-rendered) is reformatted the moment the component is created:

<input type="text" data-pac-mask="99/99/9999" value="01152026">
<!-- reformatted on load to 01/15/2026 -->

input[type="number"] is not supported — the field's type must be text (or another type that supports setSelectionRange(), such as tel). Use inputmode="numeric" for a numeric keyboard on mobile instead of type="number".

Multiple Masked Fields

A component can have any number of masked inputs — each one is registered independently when the component is created, no extra configuration needed:

<div id="profile">
    <input type="text" data-pac-mask="999-999-9999" data-pac-bind="value: phone">
    <input type="text" data-pac-mask="99/99/9999"   data-pac-bind="value: birthDate">
</div>

Reacting to Mask State

Two messages arrive in msgProc as a field is typed into. Because they're delivered via sendMessage() rather than a real DOM event, event.target resolves to the component's container, not the input — use event.detail.target (shown below as extended.target) to identify which field fired:

Message wParam Description
WakaMask.MSG_MASK_COMPLETE Raw value length Fires once every fillable slot is filled. extended.target is the <input>, extended.value is the formatted value.
WakaMask.MSG_MASK_REJECT Rejected character code Fires when a typed character doesn't match the slot it would land on (e.g. a letter typed into a digit slot). extended.target is the <input>. Does not fire when the mask is simply full — that's expected behavior, not a rejection, the same as hitting maxlength on a plain input.

Distinguishing fields when a component has more than one masked input:

msgProc(event) {
    switch (event.message) {
        case WakaMask.MSG_MASK_COMPLETE:
            if (event.detail.target === phoneInput) {
                this.phoneStatus = 'Looks good';
            } else if (event.detail.target === birthDateInput) {
                this.birthDateStatus = 'Looks good';
            }
            break;
    }
}

Programmatic Value Access

Use WakaMask.setValue() rather than assigning the bound property directly — a raw assignment bypasses masking entirely and leaves the field showing an unformatted value until the next keystroke:

async init() {
    const contact = await fetch('/api/contact/42').then(r => r.json());
    WakaMask.setValue(phoneInput, contact.phone); // formats + fires a real 'input' event
}

this.phone in the abstraction always holds the formatted display value. To get the raw significant characters (e.g. for an API payload), use WakaMask.getRawValue() rather than parsing the formatted string yourself:

submit() {
    const digits = WakaMask.getRawValue(phoneInput); // "5551234567"
    this._http.post('/api/contact', { phone: digits });
}

API

wakaPAC.use(WakaMask)

Registers the WakaMask plugin with the wakaPAC runtime.

ParameterTypeDescription
WakaMaskobjectThe WakaMask plugin object. Must be called before any wakaPAC() calls so masked inputs are scanned and registered when their component is created.
Returns void

WakaMask.setValue(input, value)

Programmatically sets a masked input's value. Accepts raw digits/letters or an already-formatted string — non-mask characters are stripped and the result is reformatted against the input's pattern. Fires a real input event so any two-way binding on the field stays in sync.

ParameterTypeDescription
inputHTMLInputElementA registered masked input (has data-pac-mask and was present when its component was created).
valuestringRaw or formatted value. Characters that don't match the mask's tokens are dropped; literals already present are discarded and rebuilt rather than matched positionally.
Returns void

WakaMask.getRawValue(input)

Returns the significant characters of a masked input's current value, with literals stripped out.

ParameterTypeDescription
inputHTMLInputElementA registered masked input.
Returns stringThe raw significant characters, e.g. "5551234567" for a field displaying 555-123-4567.

Notes

input[type="number"] is not supported. The spec forbids calling setSelectionRange() on it, which WakaMask needs for caret placement while typing. Use type="text" with inputmode="numeric" for numeric masks instead.

Inputs added to the DOM after their container's component is created — for example, rows appended inside a foreach after the initial render — are not automatically picked up. Registration happens once, when the component is created, by scanning the container for [data-pac-mask] elements.

IME composition is not intercepted (event.key === 'Process' during composition). Composed text lands in the field unmasked; the field reformats correctly on the next plain keystroke, paste, or WakaMask.setValue() call, but not while actively composing.

MSG_MASK_COMPLETE and MSG_MASK_REJECT are derived from wakaPAC.MSG_PLUGIN + 0x200 and + 0x201. WakaYouTube and WakaVideo use the 0x1000x10C range. If you have other plugins installed, confirm their message offsets don't collide with WakaMask's.

Two value-rewriting plugins should not be installed on the same input. WakaMask intercepts MSG_KEYDOWN/MSG_CHAR/MSG_PASTE globally via a message hook and rewrites input.value directly; a second plugin doing the same on the same field has no way to know WakaMask already handled the keystroke, and the two will corrupt each other's output.

Best Practices

  • Register before creating components — call wakaPAC.use(WakaMask) before any wakaPAC() calls so masked inputs are found and registered during component creation.
  • Pair with data-pac-bind for two-way bindingdata-pac-mask only controls formatting. Without a data-pac-bind="value: ..." alongside it, the field will format correctly but nothing in the abstraction will reflect its value.
  • Read event.detail.target, not event.targetMSG_MASK_COMPLETE/MSG_MASK_REJECT are delivered via sendMessage(), so event.target resolves to the container, not the input. This only matters once a component has more than one masked field, but it's easy to get burned by later.
  • Use WakaMask.setValue() for programmatic updates — assigning the bound property directly bypasses masking and leaves the field showing an unformatted value until the next keystroke.
  • Use WakaMask.getRawValue() for payloads — don't parse the formatted display value yourself; the plugin already knows which characters are literals.
  • Don't rely on MSG_MASK_REJECT for "field is full" — that case is silent by design, the same as hitting maxlength on a plain input. The message only fires for a genuine type mismatch mid-mask.