mother-mask 3.41.0 → 3.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +1142 -0
  2. package/dist/alpine.cjs +2 -0
  3. package/dist/alpine.cjs.map +1 -0
  4. package/dist/alpine.d.cts +44 -0
  5. package/dist/alpine.d.mts +44 -0
  6. package/dist/alpine.mjs +2 -0
  7. package/dist/alpine.mjs.map +1 -0
  8. package/dist/angular.cjs +2 -0
  9. package/dist/angular.cjs.map +1 -0
  10. package/dist/angular.d.cts +98 -0
  11. package/dist/angular.d.mts +98 -0
  12. package/dist/angular.mjs +2 -0
  13. package/dist/angular.mjs.map +1 -0
  14. package/dist/ember.cjs +2 -0
  15. package/dist/ember.cjs.map +1 -0
  16. package/dist/ember.d.cts +103 -0
  17. package/dist/ember.d.mts +103 -0
  18. package/dist/ember.mjs +2 -0
  19. package/dist/ember.mjs.map +1 -0
  20. package/dist/inferno.cjs +2 -0
  21. package/dist/inferno.cjs.map +1 -0
  22. package/dist/inferno.d.cts +58 -0
  23. package/dist/inferno.d.mts +58 -0
  24. package/dist/inferno.mjs +2 -0
  25. package/dist/inferno.mjs.map +1 -0
  26. package/dist/knockout.cjs +2 -0
  27. package/dist/knockout.cjs.map +1 -0
  28. package/dist/knockout.d.cts +44 -0
  29. package/dist/knockout.d.mts +44 -0
  30. package/dist/knockout.mjs +2 -0
  31. package/dist/knockout.mjs.map +1 -0
  32. package/dist/lit.cjs +21 -0
  33. package/dist/lit.cjs.map +1 -0
  34. package/dist/lit.d.cts +99 -0
  35. package/dist/lit.d.mts +99 -0
  36. package/dist/lit.mjs +21 -0
  37. package/dist/lit.mjs.map +1 -0
  38. package/dist/mithril.cjs +2 -0
  39. package/dist/mithril.cjs.map +1 -0
  40. package/dist/mithril.d.cts +48 -0
  41. package/dist/mithril.d.mts +48 -0
  42. package/dist/mithril.mjs +2 -0
  43. package/dist/mithril.mjs.map +1 -0
  44. package/dist/mother-mask.cjs +1 -1
  45. package/dist/mother-mask.cjs.map +1 -1
  46. package/dist/mother-mask.mjs +1 -1
  47. package/dist/mother-mask.mjs.map +1 -1
  48. package/dist/mother-mask.umd.js +1 -1
  49. package/dist/mother-mask.umd.js.map +1 -1
  50. package/dist/preact.cjs +2 -0
  51. package/dist/preact.cjs.map +1 -0
  52. package/dist/preact.d.cts +43 -0
  53. package/dist/preact.d.mts +43 -0
  54. package/dist/preact.mjs +2 -0
  55. package/dist/preact.mjs.map +1 -0
  56. package/dist/qwik/InputDecimal.d.ts +9 -0
  57. package/dist/qwik/InputMask.d.ts +22 -0
  58. package/dist/qwik/index.d.ts +5 -0
  59. package/dist/qwik/index.qwik.mjs +75 -0
  60. package/dist/riot.cjs +2 -0
  61. package/dist/riot.cjs.map +1 -0
  62. package/dist/riot.d.cts +76 -0
  63. package/dist/riot.d.mts +76 -0
  64. package/dist/riot.mjs +2 -0
  65. package/dist/riot.mjs.map +1 -0
  66. package/dist/solid.cjs +2 -0
  67. package/dist/solid.cjs.map +1 -0
  68. package/dist/solid.d.cts +54 -0
  69. package/dist/solid.d.mts +54 -0
  70. package/dist/solid.mjs +2 -0
  71. package/dist/solid.mjs.map +1 -0
  72. package/dist/stencil/index.d.ts +22 -0
  73. package/dist/stencil/index.js +1 -0
  74. package/dist/stencil/p-BOEnjTpG.js +1 -0
  75. package/dist/stencil/stencil-mask-decimal.d.ts +20 -0
  76. package/dist/stencil/stencil-mask-decimal.js +1 -0
  77. package/dist/stencil/stencil-mask-input.d.ts +27 -0
  78. package/dist/stencil/stencil-mask-input.js +1 -0
  79. package/dist/svelte.cjs +2 -0
  80. package/dist/svelte.cjs.map +1 -0
  81. package/dist/svelte.d.cts +44 -0
  82. package/dist/svelte.d.mts +44 -0
  83. package/dist/svelte.mjs +2 -0
  84. package/dist/svelte.mjs.map +1 -0
  85. package/dist/vue.cjs +2 -0
  86. package/dist/vue.cjs.map +1 -0
  87. package/dist/vue.d.cts +86 -0
  88. package/dist/vue.d.mts +86 -0
  89. package/dist/vue.mjs +2 -0
  90. package/dist/vue.mjs.map +1 -0
  91. package/dist/web-components.cjs +2 -0
  92. package/dist/web-components.cjs.map +1 -0
  93. package/dist/web-components.d.cts +93 -0
  94. package/dist/web-components.d.mts +93 -0
  95. package/dist/web-components.mjs +2 -0
  96. package/dist/web-components.mjs.map +1 -0
  97. package/package.json +235 -7
  98. package/src/octane/InputDecimal.tsx +85 -0
  99. package/src/octane/InputMask.tsx +116 -0
  100. package/src/octane/index.ts +9 -0
package/README.md CHANGED
@@ -169,6 +169,1148 @@ server-only code can continue importing pure helpers from `mother-mask`.
169
169
  See the [React example](https://github.com/dan2dev/mother-mask/tree/main/examples/react-simple)
170
170
  for a runnable app and browser lifecycle/memory tests.
171
171
 
172
+ ## Vue
173
+
174
+ Import the Vue 3.5 Composition API components from the separate `mother-mask/vue` entry:
175
+
176
+ ```vue
177
+ <script setup lang="ts">
178
+ import { ref } from 'vue'
179
+ import { InputMask, InputDecimal, formatDecimalValue } from 'mother-mask/vue'
180
+
181
+ // Keep option objects stable across renders.
182
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
183
+
184
+ const phone = ref('')
185
+ const amount = ref('')
186
+ </script>
187
+
188
+ <template>
189
+ <label for="phone">Phone</label>
190
+ <InputMask id="phone" name="phone" mask="(99) 99999-9999" inputmode="tel" v-model="phone" />
191
+
192
+ <label for="amount">Amount</label>
193
+ <InputDecimal id="amount" name="amount" :options="currency" v-model="amount" />
194
+
195
+ <button type="button" @click="amount = formatDecimalValue(1234.5, currency)">
196
+ Set amount
197
+ </button>
198
+ </template>
199
+ ```
200
+
201
+ `mother-mask/vue` re-exports every core function, class, and type as well as
202
+ `InputMask` and `InputDecimal`. Its core exports are aliases to `mother-mask`,
203
+ sharing the same implementation and caches. The core ESM, CommonJS, and UMD
204
+ builds remain independent of Vue. Vue is an optional peer dependency required
205
+ only when importing `mother-mask/vue`; the supported version range is
206
+ `^3.5.0`. Vue is not bundled.
207
+
208
+ - Both components support `v-model` (a `modelValue` prop plus an
209
+ `update:modelValue` emit). `InputMask` accepts `mask` and optional
210
+ `BindOptions` (without `onChange`); `InputDecimal` accepts optional
211
+ `BindDecimalOptions` and emits `(value, numericValue)`.
212
+ - Binding happens in `onMounted` — a lifecycle hook that Vue never invokes
213
+ during SSR — so these components render safely on the server without a
214
+ manual `typeof window` check; hydration binds on the client as usual.
215
+ `onBeforeUnmount` disposes the binding, and it is disposed and recreated
216
+ whenever `mask`/`options` change or `modelValue` is updated externally.
217
+ - Native attributes (`id`, `name`, `class`, ...) fall through via
218
+ `inheritAttrs: false` plus `v-bind="$attrs"` semantics; `InputDecimal`
219
+ always sets `inputmode="decimal"`. Both expose their `inputRef` via
220
+ `defineExpose` for `useTemplateRef` access to the underlying `<input>`.
221
+
222
+ See the [Vue example](https://github.com/dan2dev/mother-mask/tree/main/examples/vue-simple)
223
+ for a runnable app.
224
+
225
+ ## Angular
226
+
227
+ Import the standalone directives from the separate `mother-mask/angular` entry:
228
+
229
+ ```ts
230
+ import { Component } from '@angular/core'
231
+ import { MotherMaskDecimalDirective, MotherMaskDirective, formatDecimalValue } from 'mother-mask/angular'
232
+
233
+ // Keep option objects stable across change detection cycles.
234
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
235
+
236
+ @Component({
237
+ standalone: true,
238
+ imports: [MotherMaskDirective, MotherMaskDecimalDirective],
239
+ template: `
240
+ <label for="phone">Phone</label>
241
+ <input id="phone" name="phone" motherMask="(99) 99999-9999" inputmode="tel" [(value)]="phone" />
242
+
243
+ <label for="amount">Amount</label>
244
+ <input id="amount" name="amount" motherMaskDecimal [motherMaskDecimalOptions]="currency" [(value)]="amount" />
245
+
246
+ <button type="button" (click)="amount = formatDecimalValue(1234.5, currency)">Set amount</button>
247
+ `,
248
+ })
249
+ export class FormComponent {
250
+ readonly currency = currency
251
+ // Plain fields, not signals: the directives below bind `value` through
252
+ // classic @Input/@Output (see why in the notes under this example), so
253
+ // `[(value)]` assigns to this property directly on each change.
254
+ phone = ''
255
+ amount = ''
256
+ }
257
+ ```
258
+
259
+ `mother-mask/angular` re-exports every core function, class, and type as well
260
+ as `MotherMaskDirective` and `MotherMaskDecimalDirective`. Its core exports
261
+ are aliases to `mother-mask`, sharing the same implementation and caches. The
262
+ core ESM, CommonJS, and UMD builds remain independent of Angular. Angular is
263
+ an optional peer dependency required only when importing
264
+ `mother-mask/angular`; the supported version range is `^19.0.0 || ^20.0.0`.
265
+ Angular is not bundled.
266
+
267
+ - Both directives are attribute selectors (`input[motherMask]` and
268
+ `input[motherMaskDecimal]`) and support `[(value)]` two-way binding.
269
+ `MotherMaskDirective` accepts `motherMask` (a `MaskPattern`) and optional
270
+ `[motherMaskOptions]`; `MotherMaskDecimalDirective` accepts optional
271
+ `[motherMaskDecimalOptions]` and additionally emits `(numericValueChange)`.
272
+ - The binding lifecycle is Signals-driven: `ngOnChanges` pushes the current
273
+ mask/options/value into an internal `signal`, and `afterRenderEffect` —
274
+ which Angular runs only on the browser, never during server-side
275
+ rendering, so no manual `typeof window` check is needed — reactively
276
+ (re)binds whenever that signal changes. `ngOnDestroy` disposes the
277
+ binding, and it is disposed and recreated whenever the mask/options change
278
+ or `value` is updated externally.
279
+ - `value`/`valueChange` (and `numericValueChange`) use classic
280
+ `@Input`/`@Output` rather than the functional `input()`/`model()` API,
281
+ since Angular only wires signal-based inputs to template bindings through
282
+ the ngtsc compiler's static analysis — unavailable without the Angular CLI
283
+ build toolchain. Both forms compile to the same `[(value)]` template
284
+ syntax for consumers.
285
+ - `MotherMaskDecimalDirective` always sets `inputmode="decimal"` via a host
286
+ binding.
287
+
288
+ See the [Angular example](https://github.com/dan2dev/mother-mask/tree/main/examples/angular-simple)
289
+ for a runnable app.
290
+
291
+ ## Svelte
292
+
293
+ Import the Svelte 5 actions from the separate `mother-mask/svelte` entry:
294
+
295
+ ```svelte
296
+ <script lang="ts">
297
+ import { motherMask, motherMaskDecimal, formatDecimalValue } from 'mother-mask/svelte'
298
+
299
+ // Keep the options object stable across renders.
300
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
301
+
302
+ let phone = $state('')
303
+ let amount = $state('')
304
+ </script>
305
+
306
+ <label for="phone">Phone</label>
307
+ <input
308
+ id="phone"
309
+ name="phone"
310
+ inputmode="tel"
311
+ use:motherMask={{ mask: '(99) 99999-9999', value: phone, onValueChange: (v) => (phone = v) }}
312
+ />
313
+
314
+ <label for="amount">Amount</label>
315
+ <input
316
+ id="amount"
317
+ name="amount"
318
+ use:motherMaskDecimal={{ options: currency, value: amount, onValueChange: (v) => (amount = v) }}
319
+ />
320
+
321
+ <button type="button" onclick={() => (amount = formatDecimalValue(1234.5, currency))}>
322
+ Set amount
323
+ </button>
324
+ ```
325
+
326
+ `mother-mask/svelte` re-exports every core function, class, and type as well
327
+ as the `motherMask` and `motherMaskDecimal` actions. Its core exports are
328
+ aliases to `mother-mask`, sharing the same implementation and caches. The
329
+ core ESM, CommonJS, and UMD builds remain independent of Svelte, and this
330
+ entry has no runtime dependency on the `svelte` package at all — it's plain
331
+ TypeScript, usable from any Svelte 5 (or 4) component via `use:`.
332
+
333
+ - Actions run only once Svelte mounts the element to the live DOM, and are
334
+ torn down when it leaves — Svelte never invokes `use:` directives during
335
+ server-side rendering, so this is SSR-safe with no `typeof window` guard.
336
+ `destroy()` guarantees `dispose()` runs exactly once.
337
+ - `update(params)` is Svelte's own reactivity hook: whenever a `$state`
338
+ value read inside the `use:motherMask={...}` expression changes — `mask`,
339
+ `options`, or `value` — Svelte re-invokes it with the new params, and the
340
+ action rebinds only if the mask/options changed or `value` was set
341
+ externally (an echo of the action's own `onValueChange` is ignored).
342
+ - `motherMask` takes `{ mask, options?, value?, onValueChange? }`;
343
+ `motherMaskDecimal` takes `{ options?, value?, onValueChange? }` and calls
344
+ `onValueChange(value, numericValue)`. `motherMaskDecimal` always sets
345
+ `inputmode="decimal"` on mount.
346
+
347
+ See the [Svelte example](https://github.com/dan2dev/mother-mask/tree/main/examples/svelte-simple)
348
+ for a runnable app.
349
+
350
+ ## SolidJS
351
+
352
+ Import the custom directives from the separate `mother-mask/solid` entry:
353
+
354
+ ```tsx
355
+ import { createSignal } from 'solid-js'
356
+ import { motherMask, motherMaskDecimal, formatDecimalValue } from 'mother-mask/solid'
357
+
358
+ // Keep the options object stable across renders.
359
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
360
+
361
+ export function Form() {
362
+ const [phone, setPhone] = createSignal('')
363
+ const [amount, setAmount] = createSignal('')
364
+
365
+ return (
366
+ <>
367
+ <label for="phone">Phone</label>
368
+ <input
369
+ id="phone"
370
+ name="phone"
371
+ inputmode="tel"
372
+ use:motherMask={{ mask: '(99) 99999-9999', value: phone(), onValueChange: setPhone }}
373
+ />
374
+
375
+ <label for="amount">Amount</label>
376
+ <input
377
+ id="amount"
378
+ name="amount"
379
+ use:motherMaskDecimal={{ options: currency, value: amount(), onValueChange: setAmount }}
380
+ />
381
+
382
+ <button type="button" onClick={() => setAmount(formatDecimalValue(1234.5, currency))}>
383
+ Set amount
384
+ </button>
385
+ </>
386
+ )
387
+ }
388
+ ```
389
+
390
+ Directive props are typed via `declare module 'solid-js' { namespace JSX { interface Directives { ... } } }`,
391
+ so `use:motherMask`/`use:motherMaskDecimal` type-check without extra setup once
392
+ `mother-mask/solid` is imported anywhere in your project.
393
+
394
+ `mother-mask/solid` re-exports every core function, class, and type as well
395
+ as the `motherMask` and `motherMaskDecimal` directives. Its core exports are
396
+ aliases to `mother-mask`, sharing the same implementation and caches. The
397
+ core ESM, CommonJS, and UMD builds remain independent of Solid. Solid is an
398
+ optional peer dependency required only when importing `mother-mask/solid`;
399
+ the supported version range is `^1.9.0`. Solid is not bundled.
400
+
401
+ - Directives run when Solid mounts the element to the live DOM. Solid's
402
+ server-rendering path never instantiates real elements or invokes `use:`
403
+ callbacks, so this is SSR-safe with no `typeof window` guard needed.
404
+ - `createEffect` gives fine-grained reactivity: it tracks only the signals
405
+ actually read while building the params object passed to `use:motherMask`
406
+ (e.g. `value: phone()`), and reruns exactly when those change — rebinding
407
+ only if the mask, options, or an externally-set value actually changed
408
+ (an echo of the directive's own `onValueChange` is ignored). `onCleanup`
409
+ guarantees `dispose()` runs exactly once, both before every rebind and
410
+ when the element unmounts.
411
+ - `motherMask` takes `{ mask, options?, value?, onValueChange? }`;
412
+ `motherMaskDecimal` takes `{ options?, value?, onValueChange? }` and calls
413
+ `onValueChange(value, numericValue)`, and always sets `inputmode="decimal"`.
414
+
415
+ See the [SolidJS example](https://github.com/dan2dev/mother-mask/tree/main/examples/solid-simple)
416
+ for a runnable app.
417
+
418
+ ## Preact
419
+
420
+ Import the Preact components from the separate `mother-mask/preact` entry:
421
+
422
+ ```tsx
423
+ import { useState } from 'preact/hooks'
424
+ import { InputMask, InputDecimal, formatDecimalValue } from 'mother-mask/preact'
425
+
426
+ // Keep option objects and mask arrays stable across renders.
427
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
428
+
429
+ export function Form() {
430
+ const [phone, setPhone] = useState('')
431
+ const [amount, setAmount] = useState('')
432
+
433
+ return (
434
+ <>
435
+ <label htmlFor="phone">Phone</label>
436
+ <InputMask
437
+ id="phone"
438
+ name="phone"
439
+ mask="(99) 99999-9999"
440
+ inputMode="tel"
441
+ value={phone}
442
+ onValueChange={setPhone}
443
+ />
444
+ <label htmlFor="amount">Amount</label>
445
+ <InputDecimal
446
+ id="amount"
447
+ name="amount"
448
+ options={currency}
449
+ value={amount}
450
+ onValueChange={setAmount}
451
+ />
452
+ <button type="button" onClick={() => setAmount(formatDecimalValue(1234.5, currency))}>
453
+ Set amount
454
+ </button>
455
+ </>
456
+ )
457
+ }
458
+ ```
459
+
460
+ `mother-mask/preact` re-exports every core function, class, and type as well
461
+ as `InputMask`, `InputDecimal`, `InputMaskProps`, and `InputDecimalProps`.
462
+ Its core exports are aliases to `mother-mask`, sharing the same
463
+ implementation and caches. The core ESM, CommonJS, and UMD builds remain
464
+ independent of Preact. Preact is an optional peer dependency required only
465
+ when importing `mother-mask/preact`; the supported version range is
466
+ `^10.0.0`. Preact is not bundled, and this entry never imports
467
+ `preact/compat` — it's built directly on `preact` + `preact/hooks` to keep
468
+ Preact's footprint advantage intact.
469
+
470
+ - Both components accept controlled string `value` or uncontrolled string
471
+ `defaultValue`, same as the React version. `InputMask` accepts `mask` and
472
+ optional `BindOptions`; `InputDecimal` accepts optional
473
+ `BindDecimalOptions` and reports `(value, numericValue)`.
474
+ - Access to the underlying `<input>` is via an `inputRef` prop, not `ref`:
475
+ Preact passes `ref` on a function component straight through to the
476
+ built-in element/instance ref rather than forwarding it as a normal prop
477
+ (unlike React 19), so reusing `ref` here would require `preact/compat`'s
478
+ `forwardRef` — extra weight this entry deliberately avoids.
479
+ - This is a leaner port than the React version: it drops the IME
480
+ composition and native-`reset` edge-case handling built for React-DOM's
481
+ synthetic event layer (which doesn't exist in Preact), keeping to
482
+ `useLayoutEffect`/`useRef`/`useState` from `preact/hooks`. The dispose
483
+ guarantee is identical — every rebind and unmount releases the previous
484
+ binding first.
485
+
486
+ See the [Preact example](https://github.com/dan2dev/mother-mask/tree/main/examples/preact-simple)
487
+ for a runnable app.
488
+
489
+ ## Lit
490
+
491
+ Import the `<lit-mask-input>` and `<lit-mask-decimal>` custom elements from
492
+ the separate `mother-mask/lit` entry — importing the module registers both
493
+ elements as a side effect:
494
+
495
+ ```html
496
+ <script type="module">
497
+ import 'mother-mask/lit'
498
+ </script>
499
+
500
+ <label for="phone">Phone</label>
501
+ <lit-mask-input id="phone" name="phone" mask="(99) 99999-9999" input-mode="tel"></lit-mask-input>
502
+
503
+ <label for="amount">Amount</label>
504
+ <lit-mask-decimal id="amount" name="amount"></lit-mask-decimal>
505
+
506
+ <script type="module">
507
+ const phone = document.getElementById('phone')
508
+ phone.addEventListener('value-change', (e) => console.log(e.detail))
509
+
510
+ const amount = document.getElementById('amount')
511
+ amount.options = { decimalPlaces: 2, prefix: '$', allowNegative: true }
512
+ amount.addEventListener('numeric-value-change', (e) => console.log(e.detail))
513
+ </script>
514
+ ```
515
+
516
+ `mother-mask/lit` re-exports every core function, class, and type as well as
517
+ the `LitMaskInput` and `LitMaskDecimal` classes (for `instanceof` checks or
518
+ subclassing). Its core exports are aliases to `mother-mask`, sharing the
519
+ same implementation and caches. The core ESM, CommonJS, and UMD builds
520
+ remain independent of Lit. Lit is an optional peer dependency required only
521
+ when importing `mother-mask/lit`; the supported version range is `^3.0.0`.
522
+ Lit is not bundled.
523
+
524
+ - Both elements render into **light DOM** (`createRenderRoot()` returns
525
+ `this`, not a shadow root), so a page's own `<label for>` and global CSS
526
+ reach the native `<input>` directly, the same as any other text field.
527
+ - `<lit-mask-input>` accepts `mask` (a plain string works as an HTML
528
+ attribute; assign an array/function `MaskPattern` as a JS property) and an
529
+ `options` object (JS property only — objects can't be expressed as plain
530
+ attributes). It emits `value-change` (`detail: string`).
531
+ `<lit-mask-decimal>` accepts an `options` object and emits both
532
+ `value-change` and `numeric-value-change` (`detail: number`), and always
533
+ sets `inputmode="decimal"`.
534
+ - Binding happens in `firstUpdated` (Lit's DOM-is-ready hook) and disposes
535
+ in `disconnectedCallback` — both fire only on the browser, so there's no
536
+ `typeof window` guard to write. `updated` reactively rebinds whenever
537
+ `mask`/`options` change or `value` is set externally; an echo of the
538
+ element's own `value-change` event is ignored.
539
+ - A curated set of native attributes forward to the inner `<input>`: `name`,
540
+ `placeholder`, `disabled`, `readonly`, `required`, `input-mode`, and
541
+ `autocomplete`. `id` is a special case: `<lit-mask-input id="phone">`
542
+ writes `id="phone"` onto the custom element itself, not the `<input>` it
543
+ renders — a `<label for="phone">` would then resolve to the (unfocusable)
544
+ host instead of the actual control. After first render, the element moves
545
+ its own `id` onto the rendered `<input>` and removes it from itself, so
546
+ `<label for="phone">` keeps working exactly as if `<lit-mask-input>`
547
+ weren't there.
548
+
549
+ ## Stencil
550
+
551
+ Import the `<stencil-mask-input>` and `<stencil-mask-decimal>` custom
552
+ elements from their own `mother-mask/stencil/mask-input` and
553
+ `mother-mask/stencil/mask-decimal` entries — each registers its element as
554
+ an import side effect:
555
+
556
+ ```html
557
+ <script type="module">
558
+ import 'mother-mask/stencil/mask-input'
559
+ import 'mother-mask/stencil/mask-decimal'
560
+ </script>
561
+
562
+ <label for="phone">Phone</label>
563
+ <stencil-mask-input id="phone" name="phone" mask="(99) 99999-9999" input-mode="tel"></stencil-mask-input>
564
+
565
+ <label for="amount">Amount</label>
566
+ <stencil-mask-decimal id="amount" name="amount"></stencil-mask-decimal>
567
+
568
+ <script type="module">
569
+ const phone = document.getElementById('phone')
570
+ phone.addEventListener('value-change', (e) => console.log(e.detail))
571
+
572
+ const amount = document.getElementById('amount')
573
+ amount.options = { decimalPlaces: 2, prefix: '$', allowNegative: true }
574
+ amount.addEventListener('numeric-value-change', (e) => console.log(e.detail))
575
+ </script>
576
+ ```
577
+
578
+ Stencil components are fundamentally a compiled artifact — `@Component`,
579
+ `@Prop`, `@Event`, and friends are erased by the Stencil compiler at build
580
+ time and have no meaningful runtime implementation on their own, so (unlike
581
+ every other integration in this README) `mother-mask/stencil` is **not**
582
+ built by the same tsdown pipeline as the rest of this package; it's a
583
+ separate Stencil project ([`stencil-src/`](../../packages/mother-mask/stencil-src))
584
+ compiled with the Stencil CLI's `dist-custom-elements` output target and
585
+ copied into `dist/stencil` as part of `bun run build`. Two consequences
586
+ follow from that:
587
+
588
+ - **No `mother-mask/stencil` barrel re-exporting core helpers or a
589
+ `@stencil/core` peer dependency.** The Stencil compiler bundles
590
+ `mother-mask`'s core logic directly into each component (there's no
591
+ shared module identity/cache with a page's own `mother-mask` import, the
592
+ way the other framework entries alias `export * from 'mother-mask'`), and
593
+ bundles its own tiny runtime too (`externalRuntime: false`) — so
594
+ `<stencil-mask-input>`/`<stencil-mask-decimal>` have **zero runtime
595
+ dependencies** at all, usable on any page. Import core helpers like
596
+ `formatDecimalValue` from `mother-mask` directly.
597
+ - Each component is its own tree-shakeable module (`mask-input`,
598
+ `mask-decimal`) rather than one combined entry, matching how Stencil's
599
+ own `dist-custom-elements` output is meant to be consumed.
600
+
601
+ - `<stencil-mask-input>` accepts `mask`, `options` (JS property only), and
602
+ a curated set of forwarded attributes: `name`, `placeholder`, `disabled`,
603
+ `readonly`, `required`, and `input-mode` (exposed as the `inputModeAttr`
604
+ JS property — not `inputMode`, since this class is a real `HTMLElement`
605
+ at runtime and already declares its own incompatible `inputMode`). It
606
+ emits `value-change` (`detail: string`). `<stencil-mask-decimal>` accepts
607
+ `options` and emits both `value-change` and `numeric-value-change`
608
+ (`detail: number`), and always sets `inputmode="decimal"`.
609
+ - The ref to the native `<input>` is grabbed via `render()`'s `ref`
610
+ callback — the only point Stencil hands back a real DOM node. Binding
611
+ itself happens in `connectedCallback` when that ref is already available
612
+ (true on every *re*-connection — the element was moved or re-appended —
613
+ since the ref from the first render still exists) and otherwise in
614
+ `componentDidLoad`, which fires exactly once, right after the very first
615
+ render actually creates that ref. Either way, both only ever run in the
616
+ browser — Stencil's SSR/hydration path never calls them, so there's no
617
+ `typeof window` guard to write. `@Watch('mask')`/`@Watch('options')`/
618
+ `@Watch('value')` reactively rebind; `disconnectedCallback` always
619
+ disposes.
620
+ - Same `id`-transfer behavior as Lit's elements (both render into light
621
+ DOM): after first render, a host `id` moves onto the rendered `<input>`
622
+ so `<label for>` keeps resolving to the actual control.
623
+ - Because raw `.tsx` Stencil source can't run outside the Stencil compiler,
624
+ [`tests/stencil.test.ts`](../../packages/mother-mask/tests/stencil.test.ts)
625
+ exercises the *built* `dist/stencil` output directly rather than the
626
+ source — run `bun run build` before `bun run test` if you're working on
627
+ this entry locally.
628
+
629
+ ## Alpine.js
630
+
631
+ Register the `x-mask` directive from the separate `mother-mask/alpine`
632
+ entry with `Alpine.plugin(...)`:
633
+
634
+ ```html
635
+ <script type="module">
636
+ import Alpine from 'alpinejs'
637
+ import motherMaskPlugin from 'mother-mask/alpine'
638
+
639
+ Alpine.plugin(motherMaskPlugin)
640
+ Alpine.start()
641
+ </script>
642
+
643
+ <div x-data="{ phone: '', amount: '' }">
644
+ <label for="phone">Phone</label>
645
+ <input
646
+ id="phone"
647
+ name="phone"
648
+ inputmode="tel"
649
+ x-mask="{ mask: '(99) 99999-9999', value: phone }"
650
+ x-on:mask-change="phone = $event.detail"
651
+ />
652
+
653
+ <label for="amount">Amount</label>
654
+ <input
655
+ id="amount"
656
+ name="amount"
657
+ x-mask.decimal="{ options: { decimalPlaces: 2, prefix: '$', allowNegative: true }, value: amount }"
658
+ x-on:mask-change="amount = $event.detail"
659
+ />
660
+ </div>
661
+ ```
662
+
663
+ `mother-mask/alpine` re-exports every core function, class, and type as well
664
+ as `motherMaskPlugin` (both a named export and the module's default export,
665
+ matching Alpine's own documented plugin convention — `export default
666
+ function (Alpine) { ... }`). Its core exports are aliases to `mother-mask`,
667
+ sharing the same implementation and caches. The core ESM, CommonJS, and UMD
668
+ builds remain independent of Alpine. Alpine is an optional peer dependency
669
+ required only when importing `mother-mask/alpine`; the supported version
670
+ range is `^3.0.0`. Alpine is not bundled.
671
+
672
+ - `x-mask` takes an expression evaluating to `{ mask, options?, value? }`;
673
+ `x-mask.decimal` takes `{ options?, value? }`. Reading a reactive value
674
+ inside that expression (e.g. `value: phone`) is what makes it reactive —
675
+ Alpine's own `effect()` re-runs the directive whenever a dependency read
676
+ while evaluating the expression changes, rebinding only if the mask,
677
+ options, or an externally-set value actually changed.
678
+ - **Two-way binding goes through a DOM event, not `x-model`.** The
679
+ directive dispatches `mask-change` (`detail: string`, and for
680
+ `.decimal`, also `mask-numeric-change` with `detail: number`) — bind
681
+ those with `x-on:mask-change`, as shown above. Combining `x-mask` with
682
+ `x-model` on the same element isn't supported: both would independently
683
+ react to the native `input` event with no defined ordering between them,
684
+ and could fight over the field's live value on every keystroke.
685
+ - The directive's callback runs when Alpine walks the live DOM tree —
686
+ Alpine has no server-rendering step, so there's no `typeof window` guard
687
+ to write. `cleanup()` guarantees `dispose()` runs exactly once, both
688
+ before every rebind and when the element is removed from the DOM.
689
+
690
+ ## Native Web Components
691
+
692
+ Import the `<mm-mask-input>` and `<mm-mask-decimal>` custom elements from
693
+ the separate `mother-mask/web-components` entry — no framework at all,
694
+ plain `customElements`. Importing the module registers both elements as a
695
+ side effect:
696
+
697
+ ```html
698
+ <script type="module">
699
+ import 'mother-mask/web-components'
700
+ </script>
701
+
702
+ <label for="phone">Phone</label>
703
+ <mm-mask-input id="phone" name="phone" mask="(99) 99999-9999" input-mode="tel"></mm-mask-input>
704
+
705
+ <label for="amount">Amount</label>
706
+ <mm-mask-decimal id="amount" name="amount"></mm-mask-decimal>
707
+
708
+ <script type="module">
709
+ const phone = document.getElementById('phone')
710
+ phone.addEventListener('value-change', (e) => console.log(e.detail))
711
+
712
+ const amount = document.getElementById('amount')
713
+ amount.options = { decimalPlaces: 2, prefix: '$', allowNegative: true }
714
+ amount.addEventListener('numeric-value-change', (e) => console.log(e.detail))
715
+ </script>
716
+ ```
717
+
718
+ `mother-mask/web-components` re-exports every core function, class, and
719
+ type as well as the `MotherMaskInputElement` and `MotherMaskDecimalElement`
720
+ classes (for `instanceof` checks or subclassing). Its core exports are
721
+ aliases to `mother-mask`, sharing the same implementation and caches. The
722
+ core ESM, CommonJS, and UMD builds remain independent of this entry, and it
723
+ has **zero runtime dependencies** of its own — it's the one integration in
724
+ this README usable with no framework, no build step, and no peer
725
+ dependency at all; drop the `<script type="module">` tag on any page.
726
+
727
+ - Both elements render into **light DOM** (a single `<input>` appended as a
728
+ real child, no shadow root), so a page's own `<label for>` and global CSS
729
+ reach it directly, the same as Lit's and Stencil's entries.
730
+ - `<mm-mask-input>` accepts `mask` and `options` as either a plain
731
+ attribute (`mask`, string patterns only) or a JS property (any
732
+ `MaskPattern`, including arrays/functions, plus `options` objects, which
733
+ can't be expressed as attributes at all). A curated set of other
734
+ attributes forward to the inner `<input>`: `name`, `placeholder`,
735
+ `disabled`, `readonly`, `required`, `input-mode`, and `autocomplete`. It
736
+ emits `value-change` (`detail: string`). `<mm-mask-decimal>` accepts
737
+ `options` (JS property only) and emits both `value-change` and
738
+ `numeric-value-change` (`detail: number`), and always sets
739
+ `inputmode="decimal"`.
740
+ - Binding happens in `connectedCallback` and disposes in
741
+ `disconnectedCallback` — both fire only when a real document actually
742
+ connects/disconnects the element, so there's no `typeof window` guard to
743
+ write. `attributeChangedCallback` and the `mask`/`options`/`value`
744
+ property setters reactively rebind; an echo of the element's own
745
+ `value-change` event is ignored.
746
+ - Same `id`-transfer behavior as Lit's and Stencil's elements: after
747
+ connecting, a host `id` moves onto the rendered `<input>` so
748
+ `<label for>` keeps resolving to the actual control.
749
+
750
+ ## Qwik
751
+
752
+ Import the `InputMask` and `InputDecimal` components from the separate
753
+ `mother-mask/qwik` entry:
754
+
755
+ ```tsx
756
+ import { component$, useSignal } from '@builder.io/qwik'
757
+ import { InputMask, InputDecimal, formatDecimalValue } from 'mother-mask/qwik'
758
+
759
+ // Keep option objects outside the component so they stay stable across renders.
760
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
761
+
762
+ export default component$(() => {
763
+ const phone = useSignal('')
764
+ const amount = useSignal('')
765
+
766
+ return (
767
+ <>
768
+ <label for="phone">Phone</label>
769
+ <InputMask
770
+ id="phone"
771
+ name="phone"
772
+ mask="(99) 99999-9999"
773
+ inputMode="tel"
774
+ value={phone.value}
775
+ onValueChange$={(v) => (phone.value = v)}
776
+ />
777
+
778
+ <label for="amount">Amount</label>
779
+ <InputDecimal
780
+ id="amount"
781
+ name="amount"
782
+ options={currency}
783
+ value={amount.value}
784
+ onValueChange$={(v) => (amount.value = v)}
785
+ />
786
+
787
+ <button type="button" onClick$={() => (amount.value = formatDecimalValue(1234.5, currency))}>
788
+ Set amount
789
+ </button>
790
+ </>
791
+ )
792
+ })
793
+ ```
794
+
795
+ `mother-mask/qwik` re-exports every core function, class, and type as well
796
+ as `InputMask`, `InputDecimal`, `InputMaskProps`, and `InputDecimalProps`.
797
+ The core ESM, CommonJS, and UMD builds remain independent of Qwik. Qwik is
798
+ an optional peer dependency required only when importing `mother-mask/qwik`;
799
+ the supported version range is `^1.0.0`. Qwik's own components are not
800
+ bundled here — see below for why this entry, alone among the ones in this
801
+ README, ships close to raw source.
802
+
803
+ - Both components accept `value` and `onValueChange$` (a QRL — inline
804
+ arrow functions work; the Qwik Optimizer converts them for you). `mask`
805
+ and `options` work the same as the React version.
806
+ - Binding happens in `useVisibleTask$`, a hook Qwik only ever runs on the
807
+ client once the element is visible — Qwik's SSR renderer never executes
808
+ it, so this is safe to render on the server with no `typeof window`
809
+ guard. `track()` makes it reactive: it reruns whenever `mask`, `options`,
810
+ or `value` change, rebinding only if the mask, options, or an
811
+ externally-set value actually changed (an echo of the component's own
812
+ `onValueChange$` is ignored). `cleanup()` — called both before every
813
+ re-run and when the component unmounts — guarantees `dispose()` runs
814
+ exactly once per binding.
815
+ - `InputDecimal` defaults to `inputMode="decimal"` and reports
816
+ `onValueChange$(value, numericValue)`.
817
+
818
+ **Why this entry isn't built like the others.** `component$`, `useVisibleTask$`,
819
+ and every other `$`-suffixed Qwik API throw at runtime ("Optimizer should
820
+ replace all usages of $() ...") unless the Qwik Optimizer has already
821
+ processed them — there's no plain-JS fallback the way Lit's or Solid's
822
+ reactivity primitives have. So `mother-mask/qwik` is built with Vite's
823
+ documented ["library" mode](https://qwik.dev/docs/advanced/library/)
824
+ (`qwikVite()` + `build.lib`, see `qwik.vite.config.mjs`) rather than
825
+ tsdown, and the output (`dist/qwik/index.qwik.mjs`) deliberately keeps
826
+ `component$`/`useVisibleTask$` **unprocessed** — exactly like Qwik's own
827
+ documented component-library recipe. A consuming Qwik app's own build
828
+ (which always runs through `qwikVite` too) is what finishes optimizing
829
+ these calls into lazy-loaded segments, scoped to that app. This also means
830
+ `mother-mask/qwik` has no shared module identity/cache with a page's own
831
+ `mother-mask` import, unlike every other framework entry's
832
+ `export * from 'mother-mask'` aliasing.
833
+ - Tests for this entry run under a separate config
834
+ ([`vitest.qwik.config.ts`](../../packages/mother-mask/vitest.qwik.config.ts),
835
+ `bun run test:qwik`) — Qwik's own `@builder.io/qwik/testing` needs the
836
+ `node` environment, not `jsdom` (the two DOM implementations collide),
837
+ and `qwikVite` as a plugin isn't scoped to its own `srcDir`, so adding it
838
+ to the main config broke every other framework's unrelated JSX.
839
+
840
+ ## Inferno
841
+
842
+ Import the `InputMask` and `InputDecimal` class components from the
843
+ separate `mother-mask/inferno` entry:
844
+
845
+ ```js
846
+ import { Component, render } from 'inferno'
847
+ import { InputMask, InputDecimal, formatDecimalValue } from 'mother-mask/inferno'
848
+
849
+ // Keep option objects outside the component so they stay stable across renders.
850
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
851
+
852
+ class App extends Component {
853
+ state = { phone: '', amount: '' }
854
+
855
+ render() {
856
+ return (
857
+ <>
858
+ <label for="phone">Phone</label>
859
+ <InputMask
860
+ id="phone"
861
+ name="phone"
862
+ mask="(99) 99999-9999"
863
+ inputMode="tel"
864
+ value={this.state.phone}
865
+ onValueChange={(phone) => this.setState({ phone })}
866
+ />
867
+
868
+ <label for="amount">Amount</label>
869
+ <InputDecimal
870
+ id="amount"
871
+ name="amount"
872
+ options={currency}
873
+ value={this.state.amount}
874
+ onValueChange={(amount) => this.setState({ amount })}
875
+ />
876
+
877
+ <button type="button" onClick={() => this.setState({ amount: formatDecimalValue(1234.5, currency) })}>
878
+ Set amount
879
+ </button>
880
+ </>
881
+ )
882
+ }
883
+ }
884
+
885
+ render(<App />, document.getElementById('root'))
886
+ ```
887
+
888
+ `mother-mask/inferno` re-exports every core function, class, and type as
889
+ well as `InputMask`, `InputDecimal`, `InputMaskProps`, and
890
+ `InputDecimalProps`. Its core exports are aliases to `mother-mask`, sharing
891
+ the same implementation and caches. The core ESM, CommonJS, and UMD builds
892
+ remain independent of this entry. Inferno is an optional peer dependency
893
+ required only when importing `mother-mask/inferno`; the supported version
894
+ range is `^9.0.0`.
895
+
896
+ - Both components accept `value` and `onValueChange`. `mask` and `options`
897
+ work the same as the React version; other props are typed against
898
+ Inferno's own `InputHTMLAttributes<HTMLInputElement>`, so `name`,
899
+ `placeholder`, `inputMode`, and friends all get real autocomplete and
900
+ type-checking. `InputDecimal` always sets `inputmode="decimal"`
901
+ regardless (its own `inputMode` prop is omitted from its type, since
902
+ setting it wouldn't do anything).
903
+ - Binding happens in `componentDidMount` and disposes in
904
+ `componentWillUnmount` — Inferno's server-rendering path
905
+ (`inferno-server`) never calls either, so this is safe to render on the
906
+ server with no `typeof window` guard. `componentDidUpdate` reactively
907
+ rebinds whenever `mask`/`options` change or `value` is set externally; an
908
+ echo of a component's own `onValueChange` is ignored.
909
+ - `InputDecimal` reports `onValueChange(value, numericValue)`.
910
+ - This entry ships without JSX: Inferno's own JSX support needs
911
+ `ts-plugin-inferno`/`babel-plugin-inferno` for correct compile-time vnode
912
+ flags, which isn't worth the extra toolchain for wrapping one leaf
913
+ `<input>` — internally each component calls Inferno's `createVNode`
914
+ directly through a small `h()` helper. This only affects the library's
915
+ own source; consuming apps keep using their own JSX/Babel/TypeScript
916
+ setup exactly as shown above.
917
+
918
+ ## Octane
919
+
920
+ Import the `InputMask` and `InputDecimal` function components from the
921
+ separate `mother-mask/octane` entry:
922
+
923
+ ```tsx
924
+ /** @jsxImportSource octane */
925
+ import { useState } from 'octane'
926
+ import { InputMask, InputDecimal, formatDecimalValue } from 'mother-mask/octane'
927
+
928
+ // Keep option objects outside the component so they stay stable across renders.
929
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
930
+
931
+ export function Checkout() {
932
+ const [phone, setPhone] = useState('')
933
+ const [amount, setAmount] = useState('')
934
+
935
+ return (
936
+ <>
937
+ <label for="phone">Phone</label>
938
+ <InputMask
939
+ id="phone"
940
+ name="phone"
941
+ mask="(99) 99999-9999"
942
+ inputMode="tel"
943
+ value={phone}
944
+ onValueChange={setPhone}
945
+ />
946
+
947
+ <label for="amount">Amount</label>
948
+ <InputDecimal id="amount" name="amount" options={currency} value={amount} onValueChange={setAmount} />
949
+
950
+ <button onClick={() => setAmount(formatDecimalValue(1234.5, currency))}>Set amount</button>
951
+ </>
952
+ )
953
+ }
954
+ ```
955
+
956
+ `mother-mask/octane` re-exports every core function, class, and type as
957
+ well as `InputMask`, `InputDecimal`, `InputMaskProps`, and
958
+ `InputDecimalProps`. Its core exports are aliases to `mother-mask`, sharing
959
+ the same implementation and caches. Octane is an optional peer dependency
960
+ required only when importing `mother-mask/octane`; the supported version
961
+ range is `^0.2.0`.
962
+
963
+ - Both components accept `value` and `onValueChange`. `mask` and `options`
964
+ work the same as the React version. Access to the underlying `<input>` is
965
+ through `inputRef` rather than `ref` — see **why this entry ships raw**
966
+ below for why a plain `ref` would collide.
967
+ - Binding happens in `useLayoutEffect`, a hook Octane's server renderer
968
+ never executes — this is safe to render on the server with no
969
+ `typeof window` guard. It reconciles on every commit, rebinding only if
970
+ the mask, options, or an externally-set value actually changed (an echo
971
+ of this component's own `onValueChange` is ignored). The registered
972
+ cleanup guarantees `dispose()` runs exactly once per binding, including
973
+ on unmount.
974
+ - `InputDecimal` defaults to `inputMode="decimal"` and reports
975
+ `onValueChange(value, numericValue)`.
976
+
977
+ **Why this entry ships raw, unlike every other one in this README.**
978
+ Octane compiles function components down to direct DOM-update code keyed to
979
+ compiler-assigned hook slots — there's no virtual DOM diff at runtime, so
980
+ `useState`/`useRef`/`useLayoutEffect` only work correctly once the Octane
981
+ compiler has processed the file. Octane's own publishing guidance for
982
+ component libraries is therefore to distribute source, not a pre-built
983
+ bundle, and let the *consuming app's* own Octane toolchain (Vite, Rspack,
984
+ or Rsbuild, all via an official Octane plugin) compile it — that plugin
985
+ already knows to look inside `node_modules` for Octane-owned files, so no
986
+ extra bundler configuration is needed on the consumer's side. Accordingly,
987
+ `"./octane"` in this package's `exports` map points straight at
988
+ `src/octane/index.ts` (and `src/octane/*.tsx`) rather than at anything in
989
+ `dist/`, and this is the one entry in this README with no `tsdown` build
990
+ step of its own. The `/** @jsxImportSource octane */` pragma at the top of
991
+ each `.tsx` file is what opts a plain-TSX file into Octane's compiler (its
992
+ Vite/Rspack plugins otherwise only claim `.tsrx` files by default).
993
+ Tests for this entry run under a separate config
994
+ ([`vitest.octane.config.ts`](../../packages/mother-mask/vitest.octane.config.ts),
995
+ `bun run test:octane`) with the Octane Vite plugin scoped to just that one
996
+ test file, mirroring how Qwik's tests are isolated.
997
+
998
+ ## Mithril.js
999
+
1000
+ Import the `InputMask` and `InputDecimal` closure components from the
1001
+ separate `mother-mask/mithril` entry:
1002
+
1003
+ ```js
1004
+ import m from 'mithril'
1005
+ import { InputMask, InputDecimal, formatDecimalValue } from 'mother-mask/mithril'
1006
+
1007
+ // Keep option objects outside the view so they stay stable across redraws.
1008
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
1009
+
1010
+ let phone = ''
1011
+ let amount = ''
1012
+
1013
+ const Checkout = {
1014
+ view: () =>
1015
+ m('div', [
1016
+ m('label', { for: 'phone' }, 'Phone'),
1017
+ m(InputMask, {
1018
+ id: 'phone',
1019
+ name: 'phone',
1020
+ mask: '(99) 99999-9999',
1021
+ inputmode: 'tel',
1022
+ value: phone,
1023
+ onValueChange: (v) => { phone = v },
1024
+ }),
1025
+
1026
+ m('label', { for: 'amount' }, 'Amount'),
1027
+ m(InputDecimal, {
1028
+ id: 'amount',
1029
+ name: 'amount',
1030
+ options: currency,
1031
+ value: amount,
1032
+ onValueChange: (v) => { amount = v },
1033
+ }),
1034
+
1035
+ m('button', { onclick: () => { amount = formatDecimalValue(1234.5, currency) } }, 'Set amount'),
1036
+ ]),
1037
+ }
1038
+
1039
+ m.mount(document.body, Checkout)
1040
+ ```
1041
+
1042
+ `mother-mask/mithril` re-exports every core function, class, and type as
1043
+ well as `InputMask`, `InputDecimal`, `InputMaskAttrs`, and
1044
+ `InputDecimalAttrs`. Its core exports are aliases to `mother-mask`, sharing
1045
+ the same implementation and caches. The core ESM, CommonJS, and UMD builds
1046
+ remain independent of this entry. Mithril is an optional peer dependency
1047
+ required only when importing `mother-mask/mithril`; the supported version
1048
+ range is `^2.0.0`.
1049
+
1050
+ - Both components accept `value` and `onValueChange`; `mask` and `options`
1051
+ work the same as the React version.
1052
+ - Binding happens in `oncreate` and disposes in `onremove` — Mithril's
1053
+ server-side rendering never calls either, so this is safe to render on
1054
+ the server with no `typeof window` guard. `onupdate` reactively rebinds
1055
+ whenever `mask`/`options` change or `value` is set externally, ignoring
1056
+ an echo of this component's own `onValueChange`. Elements with any of
1057
+ these three hooks are exempt from Mithril's DOM-node recycling, so the
1058
+ same `<input>` is reused across redraws for as long as the vnode stays
1059
+ mounted.
1060
+ - `value` is deliberately never forwarded as an `m('input', ...)`
1061
+ attribute: Mithril's vdom diff would otherwise reassign `.value` from
1062
+ `vnode.attrs` on every redraw, fighting the mask's own intermediate
1063
+ edits — the wrapper controls the DOM value directly instead, exactly
1064
+ like every other framework entry in this package.
1065
+ - `InputDecimal` always sets `inputmode="decimal"` and reports
1066
+ `onValueChange(value, numericValue)`.
1067
+
1068
+ ## Ember.js
1069
+
1070
+ Import the `maskInput` and `maskDecimal` modifiers from the separate
1071
+ `mother-mask/ember` entry:
1072
+
1073
+ ```hbs
1074
+ {{! app/components/checkout.gjs / .gts, or a classic template }}
1075
+ import { maskInput, maskDecimal } from 'mother-mask/ember';
1076
+ import { formatDecimalValue } from 'mother-mask';
1077
+
1078
+ <template>
1079
+ <label for="phone">Phone</label>
1080
+ <input
1081
+ id="phone"
1082
+ name="phone"
1083
+ {{maskInput "(99) 99999-9999" value=this.phone onValueChange=this.setPhone}}
1084
+ />
1085
+
1086
+ <label for="amount">Amount</label>
1087
+ <input
1088
+ id="amount"
1089
+ name="amount"
1090
+ {{maskDecimal options=this.currency value=this.amount onValueChange=this.setAmount}}
1091
+ />
1092
+
1093
+ <button type="button" {{on "click" this.setPreset}}>Set amount</button>
1094
+ </template>
1095
+ ```
1096
+
1097
+ ```js
1098
+ import Component from '@glimmer/component';
1099
+ import { tracked } from '@glimmer/tracking';
1100
+ import { action } from '@ember/object';
1101
+
1102
+ export default class Checkout extends Component {
1103
+ // Keep option objects outside the class body's reactive graph so they
1104
+ // stay stable across renders — a fresh object literal here would read as
1105
+ // a changed `options` argument on every re-render.
1106
+ currency = { decimalPlaces: 2, prefix: '$', allowNegative: true };
1107
+
1108
+ @tracked phone = '';
1109
+ @tracked amount = '';
1110
+
1111
+ @action setPhone(value) { this.phone = value; }
1112
+ @action setAmount(value) { this.amount = value; }
1113
+ @action setPreset() { this.amount = formatDecimalValue(1234.5, this.currency); }
1114
+ }
1115
+ ```
1116
+
1117
+ `mother-mask/ember` re-exports every core function, class, and type as well
1118
+ as `maskInput`, `maskDecimal`, `maskInputModifier`, `maskDecimalModifier`,
1119
+ `MaskInputNamedArgs`, and `MaskDecimalNamedArgs`. Its core exports are
1120
+ aliases to `mother-mask`, sharing the same implementation and caches. The
1121
+ core ESM, CommonJS, and UMD builds remain independent of this entry.
1122
+ `ember-modifier` is an optional peer dependency required only when
1123
+ importing `mother-mask/ember`; the supported version range is `^4.0.0`
1124
+ (ships in every app generated by current `ember-cli`).
1125
+
1126
+ - `maskInput` takes the mask as its one positional argument
1127
+ (`{{maskInput "999-999" ...}}`); `maskDecimal` takes none. Both accept
1128
+ `value` and `onValueChange` as named arguments, plus `options`.
1129
+ - Binding happens the first time the element the modifier is attached to is
1130
+ inserted into the document, and only on the client: Ember's server-side
1131
+ rendering (FastBoot) renders to a string and never attaches real elements
1132
+ or runs modifiers, so there is no `typeof window` guard to write.
1133
+ `ember-modifier`'s own auto-tracking reruns the modifier — tearing down
1134
+ the previous binding first — whenever an argument it reads changes,
1135
+ including the mask, `options`, or an externally-set `value`.
1136
+ - Unlike every other framework entry in this README, that teardown-then-
1137
+ rerun happens unconditionally on **every** value change, including an
1138
+ echo of the modifier's own `onValueChange` — there's no hook to skip it
1139
+ from inside the modifier, since Ember has already torn down the previous
1140
+ binding by the time it runs again. This stays visually seamless anyway:
1141
+ formatting an already-formatted value is a no-op (the element's `.value`
1142
+ is never reassigned, so the caret never moves), and removing/re-adding
1143
+ the underlying event listeners doesn't touch focus or selection.
1144
+ - `maskDecimal` always sets `inputmode="decimal"` and reports
1145
+ `onValueChange(value, numericValue)`.
1146
+ - The masking logic lives in `maskInputModifier`/`maskDecimalModifier` —
1147
+ plain functions with the exact `(element, positional, named)` shape
1148
+ `ember-modifier` calls a function-based modifier with, exported
1149
+ separately from the `modifier()`-wrapped `maskInput`/`maskDecimal` so
1150
+ this package's own test suite can exercise them directly against a plain
1151
+ `HTMLInputElement`. That split exists because `ember-modifier` imports
1152
+ Ember framework internals (`@ember/application`, `@ember/modifier`,
1153
+ `@ember/destroyable`) that only resolve inside a real Ember app's build —
1154
+ they aren't separately installable npm packages — so the wrapped
1155
+ `modifier()` exports are verified at the type level (`tsc --noEmit`
1156
+ against `ember-modifier`'s published types) rather than by a runtime
1157
+ test in this repository.
1158
+
1159
+ ## Knockout.js
1160
+
1161
+ Import the separate `mother-mask/knockout` entry once, for its side effect
1162
+ of registering the `mask` and `maskDecimal` binding handlers on
1163
+ `ko.bindingHandlers`:
1164
+
1165
+ ```js
1166
+ import ko from 'knockout'
1167
+ import 'mother-mask/knockout'
1168
+ import { formatDecimalValue } from 'mother-mask'
1169
+
1170
+ // Keep option objects outside the view model's observables so they stay
1171
+ // stable across recomputes.
1172
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
1173
+
1174
+ function CheckoutViewModel() {
1175
+ this.phone = ko.observable('')
1176
+ this.amount = ko.observable('')
1177
+ this.setPreset = () => this.amount(formatDecimalValue(1234.5, currency))
1178
+ }
1179
+
1180
+ ko.applyBindings(new CheckoutViewModel(), document.getElementById('app'))
1181
+ ```
1182
+
1183
+ ```html
1184
+ <div id="app">
1185
+ <label for="phone">Phone</label>
1186
+ <input id="phone" name="phone"
1187
+ data-bind="mask: { mask: '(99) 99999-9999', value: phone, onValueChange: phone }">
1188
+
1189
+ <label for="amount">Amount</label>
1190
+ <input id="amount" name="amount"
1191
+ data-bind="maskDecimal: { options: { decimalPlaces: 2, prefix: '$', allowNegative: true }, value: amount, onValueChange: amount }">
1192
+
1193
+ <button data-bind="click: setPreset">Set amount</button>
1194
+ </div>
1195
+ ```
1196
+
1197
+ `mother-mask/knockout` re-exports every core function, class, and type as
1198
+ well as `maskBindingHandler`, `maskDecimalBindingHandler`,
1199
+ `MaskBindingConfig`, and `MaskDecimalBindingConfig`. Its core exports are
1200
+ aliases to `mother-mask`, sharing the same implementation and caches. The
1201
+ core ESM, CommonJS, and UMD builds remain independent of this entry.
1202
+ Knockout is an optional peer dependency required only when importing
1203
+ `mother-mask/knockout`; the supported version range is `^3.5.0`.
1204
+
1205
+ - Both bindings take a single object: `value` (a plain string, a Knockout
1206
+ observable/computed, or any accessor `ko.unwrap` understands) and
1207
+ `onValueChange`. `mask` and `options` work the same as the React version
1208
+ — `onValueChange` in the example above is passed directly as the
1209
+ observable itself (`phone`/`amount`), since calling an observable with an
1210
+ argument is how Knockout writes to it.
1211
+ - `init` binds once the element is live; `update` re-runs whenever an
1212
+ observable read while evaluating the binding's object literal changes —
1213
+ Knockout's own dependency tracking — rebinding only if the mask, options,
1214
+ or an externally-set value actually changed. An echo of the binding's own
1215
+ `onValueChange` is ignored.
1216
+ - `ko.utils.domNodeDisposal.addDisposeCallback` guarantees `dispose()` runs
1217
+ exactly once, both before every rebind and when Knockout removes the
1218
+ element (`ko.removeNode`, `ko.cleanNode`, or an `if`/`foreach`/template
1219
+ removing it). Knockout never touches real DOM nodes during any
1220
+ server-side step, since it has none — `applyBindings` only ever runs
1221
+ against a live document, so there is no `typeof window` guard to write.
1222
+ - `maskDecimal` always sets `inputmode="decimal"` and reports
1223
+ `onValueChange(value, numericValue)`.
1224
+ - Binding state (the live `dispose()`, the last-emitted value, the last
1225
+ mask/options) lives in a `WeakMap` keyed by the element, not in the
1226
+ handler's own closure: `ko.bindingHandlers.mask` is one object shared
1227
+ across every element that uses the binding, unlike a fresh-per-instance
1228
+ factory (a React hook call, Vue's `setup()`, …) elsewhere in this package.
1229
+
1230
+ ## Riot.js
1231
+
1232
+ Import the `maskInput` and `maskDecimal` pure-component factories from the
1233
+ separate `mother-mask/riot` entry:
1234
+
1235
+ ```js
1236
+ import { pure } from 'riot'
1237
+ import { maskInput, maskDecimal, formatDecimalValue } from 'mother-mask/riot'
1238
+
1239
+ // Keep option objects module-level so they stay stable across updates.
1240
+ const currency = { decimalPlaces: 2, prefix: '$', allowNegative: true }
1241
+
1242
+ let phone = ''
1243
+ const phoneField = pure(maskInput)({
1244
+ props: {
1245
+ mask: '(99) 99999-9999',
1246
+ name: 'phone',
1247
+ inputMode: 'tel',
1248
+ autocomplete: 'tel',
1249
+ value: phone,
1250
+ onValueChange: (v) => (phone = v),
1251
+ },
1252
+ })
1253
+ phoneField.mount(document.getElementById('phone'))
1254
+
1255
+ let amount = ''
1256
+ const amountField = pure(maskDecimal)({
1257
+ props: { options: currency, name: 'amount', value: amount, onValueChange: (v) => (amount = v) },
1258
+ })
1259
+ amountField.mount(document.getElementById('amount'))
1260
+
1261
+ // Later, e.g. from a parent Riot component's onUpdated:
1262
+ amountField.update({ options: currency, value: formatDecimalValue(1234.5, currency) })
1263
+ ```
1264
+
1265
+ ```html
1266
+ <label for="phone">Phone</label>
1267
+ <span id="phone"></span>
1268
+
1269
+ <label for="amount">Amount</label>
1270
+ <span id="amount"></span>
1271
+ ```
1272
+
1273
+ `mother-mask/riot` re-exports every core function, class, and type as well
1274
+ as `maskInput`, `maskDecimal`, `MaskInputProps`, and `MaskDecimalProps`. Its
1275
+ core exports are aliases to `mother-mask`, sharing the same implementation
1276
+ and caches. The core ESM, CommonJS, and UMD builds remain independent of
1277
+ this entry. Riot is an optional peer dependency required only when
1278
+ importing `mother-mask/riot`; the supported version range is `^10.0.0`.
1279
+
1280
+ - Both factories accept `value` and `onValueChange`; `mask` (on `maskInput`
1281
+ only) and `options` work the same as the React version. A curated set of
1282
+ other props forward as plain attributes: `name`, `placeholder`,
1283
+ `inputMode` (rendered as `inputmode`, `maskInput` only — `maskDecimal`
1284
+ always sets it to `"decimal"`), `autocomplete`, `disabled`, `readonly`,
1285
+ and `required`.
1286
+ - Renders into light DOM: the `<input>` is appended as a real child of the
1287
+ host element the component mounts onto, so a page's own `<label for>` and
1288
+ global CSS reach it directly, the same as this package's Lit/Stencil/Web
1289
+ Components entries. A host `id` is moved onto the `<input>` once, right
1290
+ after mounting, for the same reason those entries do it.
1291
+ - `maskDecimal` always sets `inputmode="decimal"` and reports
1292
+ `onValueChange(value, numericValue)`.
1293
+
1294
+ **Why this entry uses Riot's `pure()` instead of a compiled `.riot`
1295
+ component.** `pure()` is Riot's own documented escape hatch for
1296
+ mounting non-Riot-templated content — third-party libraries, plain DOM —
1297
+ as a node in a Riot component tree, bypassing the `.riot`-file/compiler
1298
+ pipeline entirely; that's a direct fit here, since this wrapper's only job
1299
+ is to own one plain `<input>` imperatively, exactly like every other
1300
+ framework entry in this package. `riot.pure`'s own lifecycle names —
1301
+ `mount`/`update`/`unmount` — serve the same purpose as a full `.riot`
1302
+ component's `onMounted`/`onUnmounted`, with one difference worth calling
1303
+ out: `update()` isn't automatically re-invoked by reactive tracking the
1304
+ way `onUpdated` is on a full component. A parent must call this
1305
+ component's own `update(props)` explicitly (typically from its own
1306
+ `onUpdated`, or right after changing props) to reformat for a new
1307
+ `mask`/`options`/`value` — `sync()` still rebinds only if the mask,
1308
+ options, or an externally-set value actually changed, and still ignores an
1309
+ echo of this component's own `onValueChange`, exactly like every other
1310
+ framework entry. `mount`/`update`/`unmount` only ever run on the client:
1311
+ Riot has no built-in server-side renderer of its own, so there is no
1312
+ `typeof window` guard to write.
1313
+
172
1314
  ## Decimal Inputs
173
1315
 
174
1316
  Use `bindDecimal` for numbers, currency fields, and values where the integer part should grow freely.