Make InputMask native, without IMask

This commit is contained in:
Bartek committed 2026-09-30 14:00:48 +02:00
1 parent 9c7afc5002
commit b9499d4cdd
6 files changed
+543 -116

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@tabler/core": minor
"@tabler/docs": patch
"@tabler/preview": patch
---
`InputMask` no longer needs IMask. It formats `data-mask` patterns natively, with the same `0`, `a` and `*` characters, and adds the `tokens` and `placeholderChar` options, a function as `mask`, the `unmaskedValue` and `isComplete` properties and the `accept.bs.input-mask` and `complete.bs.input-mask` events. IMask object masks (`Number`, `Date`, a RegExp) still work when IMask is loaded, but are deprecated.
+247 -15
View File
@@ -6,10 +6,13 @@
*/
import BaseComponent from './bootstrap/base-component'
import EventHandler from './bootstrap/dom/event-handler'
import { initAll } from './bootstrap/util/component-functions'
import type { ElementSelector } from './bootstrap/types'
// The part of an IMask instance the component uses
type MaskFunction = (unmaskedValue: string) => string
// The part of an IMask instance the legacy fallback uses
interface IMaskInstance {
value: string
unmaskedValue: string
@@ -19,9 +22,14 @@ interface IMaskInstance {
}
type ComponentConfig = {
/** an IMask mask: a pattern string, `Number`, `Date`, a RegExp or a mask object */
mask: unknown
/** a pattern string, or a function that returns one for the current unmasked value */
mask: string | MaskFunction
/** show the mask only as far as the user has typed */
lazy: boolean
/** the character that stands for an empty slot when `lazy` is `false` */
placeholderChar: string
/** extra pattern characters, each with the RegExp one typed character has to match */
tokens: Record<string, RegExp>
}
type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
@@ -31,43 +39,96 @@ type ComponentConfigInput = Partial<ComponentConfig> & Record<string, unknown>
*/
const NAME = 'input-mask'
const DATA_KEY = `bs.${NAME}`
const EVENT_KEY = `.${DATA_KEY}`
const EVENT_ACCEPT = `accept${EVENT_KEY}`
const EVENT_COMPLETE = `complete${EVENT_KEY}`
const ATTRIBUTE_MASK = 'data-mask'
const ATTRIBUTE_VISIBLE = 'data-mask-visible'
const ATTRIBUTE_PLACEHOLDER_CHAR = 'data-mask-placeholder-char'
const SELECTOR_DATA_MASK = `[${ATTRIBUTE_MASK}]`
const ESCAPE = '\\'
const TOKENS: Record<string, RegExp> = {
'0': /\d/,
'a': /\p{L}/u,
'*': /./,
}
const Default: ComponentConfig = {
mask: '',
lazy: true,
placeholderChar: '_',
tokens: {},
}
const DefaultType: Record<keyof ComponentConfig, string> = {
mask: '(string|function|regexp|object|array)',
lazy: 'boolean',
placeholderChar: 'string',
tokens: 'object',
}
const isLegacyMask = (mask: unknown): boolean => typeof mask !== 'string' && (typeof mask !== 'function' || mask === Number || mask === Date)
type Slot = { token: RegExp | null; char: string }
// `\0` makes a literal of a token character; every other character outside the token map is a literal
const parse = (mask: string, tokens: Record<string, RegExp>): Slot[] => {
const slots: Slot[] = []
for (let i = 0; i < mask.length; i++) {
const char = mask[i]
if (char === ESCAPE && i + 1 < mask.length) {
slots.push({ token: null, char: mask[++i] })
} else {
slots.push({ token: tokens[char] ?? null, char })
}
}
return slots
}
/**
* Class definition
*
* Wraps the IMask plugin (https://imask.js.org), loaded separately as
* `window.IMask`. Without the plugin the component is inert.
* A native input mask. Pattern characters: `0` a digit, `a` a letter, `*` any
* character; `\` makes the next one a literal. Add more with the `tokens` option.
*/
class InputMask extends BaseComponent {
declare _element: HTMLInputElement
declare _config: ComponentConfig
_mask: IMaskInstance | null = null
_legacy: IMaskInstance | null = null
_unmasked = ''
_complete = false
_onInput = (event: Event): void => this._handleInput(event as InputEvent)
constructor(element: ElementSelector, config?: ComponentConfigInput) {
super(element, config)
if (!this._element || !this._config.mask || !window.IMask) {
if (!this._element || !this._config.mask) {
return
}
// Every option from the config reaches IMask, not only the two typed ones.
this._mask = new window.IMask(this._element, { ...this._config })
// deprecated(2.0): IMask object masks (`Number`, `Date`, a RegExp) stay on the IMask library when it is loaded
if (isLegacyMask(this._config.mask)) {
if (window.IMask) {
this._legacy = new window.IMask(this._element, { ...this._config })
} else {
console.warn('Tabler InputMask: only string and function masks are supported without IMask.')
}
return
}
this._element.addEventListener('input', this._onInput)
this.update()
}
// Getters
@@ -83,32 +144,203 @@ class InputMask extends BaseComponent {
return NAME
}
/** The IMask instance, for options the component does not expose. */
get mask(): IMaskInstance | null {
return this._mask
/** The value as shown in the input, with the literals. */
get value(): string {
return this._legacy ? this._legacy.value : this._element.value
}
/** Only what the user typed, without the literals and the placeholders. */
get unmaskedValue(): string {
return this._legacy ? this._legacy.unmaskedValue : this._unmasked
}
/** `true` when every slot of the mask is filled. */
get isComplete(): boolean {
return this._complete
}
// deprecated(2.0): the IMask instance; the component itself answers the same members
get mask(): this | IMaskInstance | null {
return this._legacy ?? (this._config.mask ? this : null)
}
// Public
/** Re-applies the mask after the value was changed from code. */
update(): void {
this._mask?.updateValue()
if (this._legacy) {
this._legacy.updateValue()
return
}
this._unmasked = this._extract(this._element.value).unmasked
this._render(this._unmasked, null)
}
// deprecated(2.0): IMask method names
updateValue(): void {
this.update()
}
destroy(): void {
this.dispose()
}
dispose(): void {
this._mask?.destroy()
this._legacy?.destroy()
this._element.removeEventListener('input', this._onInput)
super.dispose()
}
// Private
_slots(unmasked: string): Slot[] {
const { mask, tokens } = this._config
return parse(typeof mask === 'function' ? mask(unmasked) : mask, { ...TOKENS, ...tokens })
}
_extract(raw: string, caret: number | null = null): { unmasked: string; count: number | null } {
// A dynamic mask is resolved by what the value holds, so the first pass reads it with the previous mask
let slots = this._slots(this._unmasked)
let unmasked = ''
let count: number | null = null
let position = 0
for (let i = 0; i <= raw.length; i++) {
if (i === caret) {
count = unmasked.length
}
const char = raw[i]
if (char === undefined) {
break
}
while (position < slots.length && !slots[position].token && slots[position].char !== char) {
position++
}
if (position >= slots.length) {
break
}
const { token } = slots[position]
if (!token) {
position++
continue
}
if (char !== this._config.placeholderChar && token.test(char)) {
unmasked += char
position++
slots = this._slots(unmasked)
}
}
return { unmasked, count }
}
_format(unmasked: string): { text: string; ends: number[]; complete: boolean } {
const slots = this._slots(unmasked)
const { lazy, placeholderChar } = this._config
const ends: number[] = []
let text = ''
let used = 0
let complete = true
for (const slot of slots) {
if (!slot.token) {
if (!lazy || used < unmasked.length) {
text += slot.char
}
continue
}
if (used < unmasked.length) {
text += unmasked[used++]
ends.push(text.length)
} else {
complete = false
if (lazy) {
break
}
text += placeholderChar
}
}
return { text, ends, complete: complete && slots.some((slot) => slot.token) }
}
_render(unmasked: string, typed: number | null): void {
const { text, ends, complete } = this._format(unmasked)
const changed = text !== this._element.value
if (changed) {
this._element.value = text
}
if (typed !== null && document.activeElement === this._element) {
const caret = typed === 0 ? 0 : (ends[Math.min(typed, ends.length) - 1] ?? text.length)
this._element.setSelectionRange(caret, caret)
}
if (complete !== this._complete) {
this._complete = complete
if (complete) {
EventHandler.trigger(this._element, EVENT_COMPLETE, { value: text, unmaskedValue: unmasked })
}
}
}
_handleInput(event: InputEvent): void {
if (event.isComposing) {
return
}
const raw = this._element.value
const caret = this._element.selectionStart ?? raw.length
const previous = this._unmasked
let { unmasked, count } = this._extract(raw, caret)
let typed = count ?? unmasked.length
// Deleting a literal changes nothing after re-masking, so it removes the digit next to it
if (unmasked === previous && event.inputType.startsWith('delete') && typed > 0) {
const at = event.inputType === 'deleteContentForward' ? typed : typed - 1
if (at < unmasked.length) {
unmasked = unmasked.slice(0, at) + unmasked.slice(at + 1)
typed = at
}
}
this._unmasked = unmasked
this._render(unmasked, typed)
if (unmasked !== previous) {
EventHandler.trigger(this._element, EVENT_ACCEPT, { value: this._element.value, unmaskedValue: unmasked })
}
}
_mergeConfigObj(config?: Record<string, unknown>, element?: HTMLElement): Record<string, unknown> {
// `data-mask` holds the pattern; `data-mask-visible="true"` shows it
// before typing, which is IMask's `lazy: false`.
// before typing, which is `lazy: false`.
const dataOptions: Record<string, unknown> = {}
const mask = element?.getAttribute(ATTRIBUTE_MASK)
const placeholderChar = element?.getAttribute(ATTRIBUTE_PLACEHOLDER_CHAR)
if (mask) {
dataOptions.mask = mask
}
if (placeholderChar) {
dataOptions.placeholderChar = placeholderChar
}
// A bare `data-mask-visible` means visible, like `="true"`.
if (element?.hasAttribute(ATTRIBUTE_VISIBLE)) {
dataOptions.lazy = element.getAttribute(ATTRIBUTE_VISIBLE) === 'false'
+224 -62
View File
@@ -4,28 +4,26 @@ import InputMask from '../../src/input-mask'
describe('InputMask', () => {
let fixtureEl: HTMLElement
let plugin: ReturnType<typeof vi.fn>
beforeAll(() => {
fixtureEl = getFixture()
})
beforeEach(() => {
plugin = vi.fn(function (this: Record<string, unknown>) {
this.updateValue = vi.fn()
this.destroy = vi.fn()
})
window.IMask = plugin as unknown as Window['IMask']
fixtureEl.innerHTML = '<input type="text" data-mask="00/00/0000" data-mask-visible="true">'
fixtureEl.innerHTML = '<input type="text" data-mask="(00) 0000-0000">'
})
afterEach(() => {
clearFixture()
delete window.IMask
})
const input = (): HTMLInputElement => fixtureEl.querySelector('input')!
const lastInstance = (): Record<string, ReturnType<typeof vi.fn>> => plugin.mock.instances[plugin.mock.instances.length - 1] as Record<string, ReturnType<typeof vi.fn>>
const type = (el: HTMLInputElement, value: string, inputType = 'insertText', caret = value.length): void => {
el.value = value
el.setSelectionRange(caret, caret)
el.dispatchEvent(new InputEvent('input', { inputType, bubbles: true }))
}
describe('NAME', () => {
it('should return plugin name', () => {
@@ -33,62 +31,249 @@ describe('InputMask', () => {
})
})
describe('constructor', () => {
it('should create the mask from data-mask and data-mask-visible', () => {
const instance = new InputMask(input())
describe('formatting', () => {
it('should add literals as the user types', () => {
const el = input()
const instance = new InputMask(el)
expect(plugin).toHaveBeenCalledWith(input(), { mask: '00/00/0000', lazy: false })
expect(instance.mask).toBe(plugin.mock.instances[0])
type(el, '1')
expect(el.value).toBe('(1')
type(el, '(12')
type(el, '(123')
expect(el.value).toBe('(12) 3')
expect(instance.unmaskedValue).toBe('123')
})
it('should default to a lazy mask', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="(00) 0000-0000">'
it('should drop characters the token does not accept', () => {
const el = input()
new InputMask(el)
type(el, 'a1b2')
expect(el.value).toBe('(12')
})
it('should format a pasted value and cut it to the mask', () => {
const el = input()
const instance = new InputMask(el)
type(el, '+48 (12) 3456-7890 1234', 'insertFromPaste')
expect(el.value).toBe('(48) 1234-5678')
expect(instance.unmaskedValue).toBe('4812345678')
})
it('should mask a value set before the component was created', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="00/00/0000" value="31122026">'
new InputMask(input())
expect(plugin).toHaveBeenCalledWith(input(), { mask: '(00) 0000-0000', lazy: true })
expect(input().value).toBe('31/12/2026')
})
it('should let the config object win over the attributes', () => {
new InputMask(input(), { mask: '0000', lazy: true })
it('should re-apply the mask on update()', () => {
const instance = new InputMask(input())
expect(plugin).toHaveBeenCalledWith(input(), { mask: '0000', lazy: true })
input().value = '1234567890'
instance.update()
expect(input().value).toBe('(12) 3456-7890')
})
it('should keep the caret after the typed character in the middle', () => {
const el = input()
new InputMask(el)
el.focus()
type(el, '(12) 3456-7890')
type(el, '(192) 3456-7890', 'insertText', 3)
expect(el.value).toBe('(19) 2345-6789')
expect(el.selectionStart).toBe(3)
})
})
describe('deleting', () => {
it('should remove the digit before a deleted literal', () => {
const el = input()
const instance = new InputMask(el)
type(el, '(12) 3')
type(el, '(12)3', 'deleteContentBackward', 4)
expect(instance.unmaskedValue).toBe('13')
expect(el.value).toBe('(13')
})
it('should clear a lazy mask when the value is emptied', () => {
const el = input()
new InputMask(el)
type(el, '(1')
type(el, '', 'deleteContentBackward', 0)
expect(el.value).toBe('')
})
})
describe('visible mask', () => {
it('should show placeholders from the start', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="00/00" data-mask-visible="true">'
new InputMask(input())
expect(input().value).toBe('__/__')
})
it('should fill the slots and treat a bare attribute as visible', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="00/00" data-mask-visible>'
const el = input()
const instance = new InputMask(el)
type(el, '1__/__')
expect(el.value).toBe('1_/__')
expect(instance.unmaskedValue).toBe('1')
})
it('should use the placeholder character from the attribute', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="00" data-mask-visible data-mask-placeholder-char="#">'
new InputMask(input())
expect(input().value).toBe('##')
})
})
describe('tokens', () => {
it('should support letters and any character', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="aa-**">'
const el = input()
new InputMask(el)
type(el, '1ab-9z')
expect(el.value).toBe('ab-9z')
})
it('should use custom tokens', () => {
const el = input()
new InputMask(el, { mask: 'HH', tokens: { H: /[0-9a-f]/i } })
type(el, 'fg9')
expect(el.value).toBe('f9')
})
it('should treat an escaped token as a literal', () => {
fixtureEl.innerHTML = ''
const el = fixtureEl.appendChild(document.createElement('input'))
new InputMask(el, { mask: '\\000', lazy: false })
expect(el.value).toBe('0__')
})
})
describe('dynamic mask', () => {
it('should pick the mask from the unmasked value', () => {
const el = input()
new InputMask(el, { mask: (value: string) => (value.startsWith('34') || value.startsWith('37') ? '0000 000000 00000' : '0000 0000 0000 0000') })
type(el, '3782822463100005')
expect(el.value).toBe('3782 822463 10000')
})
})
describe('events', () => {
it('should fire accept with the values', () => {
const el = input()
new InputMask(el)
const spy = vi.fn()
el.addEventListener('accept.bs.input-mask', ((event: CustomEvent) => spy(event)) as EventListener)
type(el, '12')
expect(spy).toHaveBeenCalledTimes(1)
const event = spy.mock.calls[0][0] as Event & { value: string; unmaskedValue: string }
expect(event.value).toBe('(12')
expect(event.unmaskedValue).toBe('12')
})
it('should fire complete once when every slot is filled', () => {
const el = input()
const instance = new InputMask(el)
const spy = vi.fn()
el.addEventListener('complete.bs.input-mask', spy)
type(el, '(12) 3456-789')
expect(spy).not.toHaveBeenCalled()
expect(instance.isComplete).toBe(false)
type(el, '(12) 3456-7890')
expect(spy).toHaveBeenCalledTimes(1)
expect(instance.isComplete).toBe(true)
})
})
describe('constructor', () => {
it('should let the config object win over the attributes', () => {
const el = input()
new InputMask(el, { mask: '0000' })
type(el, '12345')
expect(el.value).toBe('1234')
})
it('should stay inert without a mask', () => {
fixtureEl.innerHTML = '<input type="text" data-mask>'
const instance = new InputMask(input())
expect(plugin).not.toHaveBeenCalled()
type(input(), 'abc')
expect(input().value).toBe('abc')
expect(instance.mask).toBeNull()
})
it('should stay inert when the plugin is not loaded', () => {
delete window.IMask
it('should warn and stay inert for an IMask object mask without IMask', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
fixtureEl.innerHTML = '<input type="text">'
new InputMask(input(), { mask: Number as unknown as string })
expect(() => new InputMask(input())).not.toThrow()
expect(InputMask.getInstance(input())).toBeInstanceOf(InputMask)
expect(warn).toHaveBeenCalled()
warn.mockRestore()
})
})
describe('legacy IMask fallback', () => {
afterEach(() => {
delete window.IMask
})
it('should hand an object mask to IMask when it is loaded', () => {
const plugin = vi.fn(function (this: Record<string, unknown>) {
this.updateValue = vi.fn()
this.destroy = vi.fn()
})
window.IMask = plugin as unknown as Window['IMask']
fixtureEl.innerHTML = '<input type="text">'
const instance = new InputMask(input(), { mask: Number as unknown as string })
expect(plugin).toHaveBeenCalledTimes(1)
expect(instance.mask).toBe(plugin.mock.instances[0])
})
})
describe('public API', () => {
it('should forward update to the mask', () => {
const instance = new InputMask(input())
instance.update()
expect(lastInstance().updateValue).toHaveBeenCalledTimes(1)
})
it('should destroy the mask and remove the instance on dispose', () => {
const instance = new InputMask(input())
const element = input()
const destroy = lastInstance().destroy
it('should remove the listeners and the instance on dispose', () => {
const el = input()
const instance = new InputMask(el)
instance.dispose()
type(el, 'abc')
expect(destroy).toHaveBeenCalledTimes(1)
expect(InputMask.getInstance(element)).toBeNull()
expect(el.value).toBe('abc')
expect(InputMask.getInstance(el)).toBeNull()
})
it('should answer the IMask member names', () => {
const instance = new InputMask(input())
expect(instance.mask).toBe(instance)
expect(typeof instance.updateValue).toBe('function')
expect(typeof instance.destroy).toBe('function')
})
})
@@ -98,29 +283,6 @@ describe('InputMask', () => {
const second = InputMask.getOrCreateInstance(input())
expect(second).toBe(first)
expect(plugin).toHaveBeenCalledTimes(1)
})
})
describe('config passthrough', () => {
it('hands every config option to IMask', () => {
fixtureEl.innerHTML = '<input type="text">'
new InputMask(input(), { mask: '000', placeholderChar: '#' })
expect(plugin).toHaveBeenCalledWith(input(), { mask: '000', lazy: true, placeholderChar: '#' })
})
it('accepts a non-string mask', () => {
fixtureEl.innerHTML = '<input type="text">'
expect(() => new InputMask(input(), { mask: Number })).not.toThrow()
expect(() => new InputMask(fixtureEl.appendChild(document.createElement('input')), { mask: /^\d+$/ })).not.toThrow()
})
it('treats a bare data-mask-visible as visible', () => {
fixtureEl.innerHTML = '<input type="text" data-mask="00" data-mask-visible>'
new InputMask(input())
expect(plugin).toHaveBeenCalledWith(input(), { mask: '00', lazy: false })
})
})
})
+61 -33
View File
@@ -3,34 +3,12 @@ title: Input mask
order: 11
summary: An input mask formats what the user types to a pattern, such as a phone number or a date, which makes the expected format clear and cuts validation errors.
description: Format user input automatically with input masks for dates, phone numbers, and other patterns to reduce validation errors.
docs-libs: [imask]
related: [/ui/forms/elements]
---
import Example from '@components/Example.astro'
import TabsPackage from '@components/TabsPackage.astro'
import CodeDocs from '@components/CodeDocs.astro';
## Installation
Install the imask library:
<TabsPackage name="imask" />
And import or require:
```javascript
import IMask from 'imask'
```
Or include it from a CDN:
```html
<script src="https://cdn.jsdelivr.net/npm/imask"></script>
```
The [IMask documentation](https://imask.js.org/guide.html#installation) covers other ways to install it.
## Default markup
Use an input mask where the value has a fixed format, such as a phone number, a date or a card number.
@@ -54,39 +32,89 @@ To create an input mask, add the `data-mask` attribute to the input element:
<input type="text" id="mask-phone" name="input-mask" class="form-control" data-mask="(00) 0000-0000" data-mask-visible="true" placeholder="(00) 0000-0000" autocomplete="off" />
</Example>
## More examples
## Pattern
If you need more examples of input masks, you can find them in the [IMask documentation](https://imask.js.org/guide.html#masked-input).
| Character | Accepts |
| --- | --- |
| `0` | a digit |
| `a` | a letter |
| `*` | any character |
| `\` | makes the next character a literal, so `\0` is a plain `0` |
Every other character is a literal, and Tabler adds it for the user as they type. Paste works too: the value is cut to the pattern and the characters that do not fit are dropped.
<Example column>
<label class="form-label" for="mask-date">Date</label>
<input type="text" id="mask-date" class="form-control mb-3" data-mask="00/00/0000" placeholder="00/00/0000" autocomplete="off" />
<label class="form-label" for="mask-plate">Plate</label>
<input type="text" id="mask-plate" class="form-control" data-mask="aa-0000" data-mask-visible="true" autocomplete="off" />
</Example>
## Options
| Option | Attribute | Default | Description |
| --- | --- | --- | --- |
| `mask` | `data-mask` | `''` | The pattern, or a function that gets the unmasked value and returns the pattern. |
| `lazy` | `data-mask-visible="true"` | `true` | With `data-mask-visible` the whole pattern shows before the user types. |
| `placeholderChar` | `data-mask-placeholder-char` | `_` | The character for an empty slot when the pattern is visible. |
| `tokens` | | `{}` | Extra pattern characters, each with the RegExp one character has to match. |
## JavaScript
Tabler wraps the plugin in an `InputMask` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-mask`. This is the code that runs:
Tabler creates an `InputMask` for every element with `data-mask`, the same way as the Bootstrap components. This is the code that runs:
<CodeDocs name="input-mask-init" file="core/js/src/input-mask.ts" />
Create the component yourself for a field added later. The pattern comes from `data-mask`, or from the config object, which wins:
Create the component yourself for a field added later. The config object wins over the attributes:
```js
const phone = new tabler.InputMask(document.getElementById('phone'), { mask: '(00) 0000-0000' });
```
`data-mask-visible="true"` shows the pattern before the user types. In the config object that is `lazy: false`.
The `mask` getter returns the IMask instance, so everything from the [IMask API](https://imask.js.org/guide.html#api) is available:
A custom token, here a hexadecimal digit:
```js
const phone = tabler.InputMask.getOrCreateInstance(document.getElementById('phone'));
console.log(phone.mask.unmaskedValue);
new tabler.InputMask(document.getElementById('color'), {
mask: '#HHHHHH',
tokens: { H: /[0-9a-f]/i },
});
```
| Method | Description |
A function as the mask changes the pattern while the user types, for example for a card number:
```js
new tabler.InputMask(document.getElementById('card'), {
mask: (value) => (/^3[47]/.test(value) ? '0000 000000 00000' : '0000 0000 0000 0000'),
});
```
### Methods and properties
| Name | Description |
| --- | --- |
| `value` | The value as shown, with the literals. |
| `unmaskedValue` | Only what the user typed. Send this one to a server. |
| `isComplete` | `true` when every slot is filled. |
| `update()` | Re-applies the mask after the value was changed from code. |
| `mask` | Getter. The IMask instance, or `null` when the library is not loaded. |
| `dispose()` | Removes the mask and the component from the element. |
| `getInstance(element)` | Static. Returns the component for the element, or `null`. |
| `getOrCreateInstance(element, config)` | Static. Returns the component for the element and creates it when needed. |
### Events
The events are triggered on the input.
| Event | Description |
| --- | --- |
| `accept.bs.input-mask` | The unmasked value changed. Has `value` and `unmaskedValue`. |
| `complete.bs.input-mask` | The last slot was filled. Has `value` and `unmaskedValue`. |
```js
input.addEventListener('complete.bs.input-mask', (event) => {
console.log(event.unmaskedValue);
});
```
## Accessibility
- A mask changes what the field accepts as the user types, which is easy to lose track of without sight. Say the expected format in a hint tied to the field with `aria-describedby`.
+2 -2
View File
@@ -52,7 +52,7 @@ const rows = [
]
---
<DefaultLayout title="Form elements" pageMenu="form.elements" pageLibs={['nouislider', 'vanilla-calendar-pro', 'tom-select', 'imask']}>
<DefaultLayout title="Form elements" pageMenu="form.elements" pageLibs={['nouislider', 'vanilla-calendar-pro', 'tom-select']}>
<DocsLink slot="page-header-actions" path="/ui/forms/elements" />
<div class="row row-cards">
<div class="col-12">
@@ -133,7 +133,7 @@ const rows = [
<FormGroup label="Hour"><InputMask mask="00:00:00" visible /></FormGroup>
<FormGroup label={'Date & Hour'}><InputMask mask="00/00/0000 00:00:00" visible /></FormGroup>
<FormGroup label="ZIP Code"><InputMask mask="00000-000" visible /></FormGroup>
<FormGroup label="Money"><InputMask mask="000.000.000.000.000,00" visible reverse /></FormGroup>
<FormGroup label="Money"><InputMask mask="000.000.000.000.000,00" visible /></FormGroup>
<FormGroup label="Telephone"><InputMask mask="0000-0000" visible /></FormGroup>
<FormGroup label="Telephone with Code Area"><InputMask mask="(00) 0000-0000" visible /></FormGroup>
<FormGroup label="IP Address"><InputMask mask="099.099.099.099" placeholder="000.000.000.000" visible /></FormGroup>
+2 -4
View File
@@ -1,14 +1,12 @@
---
// IMask initializes from the data-mask attributes (tabler.js).
interface Props {
mask?: string
visible?: boolean
placeholder?: string
reverse?: boolean
}
const { mask = '00/00/0000', visible, placeholder, reverse } = Astro.props
const { mask = '00/00/0000', visible, placeholder } = Astro.props
const name = 'mask'
---
<input type="text" name={`input-${name}`} class="form-control" data-mask={mask} data-mask-visible={visible ? 'true' : undefined} placeholder={placeholder ?? mask} data-mask-reverse={reverse ? 'true' : undefined} autocomplete="off" />
<input type="text" name={`input-${name}`} class="form-control" data-mask={mask} data-mask-visible={visible ? 'true' : undefined} placeholder={placeholder ?? mask} autocomplete="off" />