Mask for an Input to allow phone numbers?
Angular5 and 6:
angular 5 and 6 recommended way is to use @HostBindings and @HostListeners instead of the host property
remove host and add @HostListener
@HostListener('ngModelChange', ['$event']) onModelChange(event) { this.onInputChange(event, false); } @HostListener('keydown.backspace', ['$event']) keydownBackspace(event) { this.onInputChange(event.target.value, true); }
Working Online stackblitz Link: https://angular6-phone-mask.stackblitz.io
Stackblitz Code example: https://stackblitz.com/edit/angular6-phone-mask
Official documentation link https://angular.io/guide/attribute-directives#respond-to-user-initiated-events
Angular2 and 4:
original
One way you could do it is using a directive that injects NgControl
and manipulates the value
(for details see inline comments)
@Directive({ selector: '[ngModel][phone]', host: { '(ngModelChange)': 'onInputChange($event)', '(keydown.backspace)': 'onInputChange($event.target.value, true)' }})export class PhoneMask { constructor(public model: NgControl) {} onInputChange(event, backspace) { // remove all mask characters (keep only numeric) var newVal = event.replace(/\D/g, ''); // special handling of backspace necessary otherwise // deleting of non-numeric characters is not recognized // this laves room for improvement for example if you delete in the // middle of the string if (backspace) { newVal = newVal.substring(0, newVal.length - 1); } // don't show braces for empty value if (newVal.length == 0) { newVal = ''; } // don't show braces for empty groups at the end else if (newVal.length <= 3) { newVal = newVal.replace(/^(\d{0,3})/, '($1)'); } else if (newVal.length <= 6) { newVal = newVal.replace(/^(\d{0,3})(\d{0,3})/, '($1) ($2)'); } else { newVal = newVal.replace(/^(\d{0,3})(\d{0,3})(.*)/, '($1) ($2)-$3'); } // set the new value this.model.valueAccessor.writeValue(newVal); }}
@Component({ selector: 'my-app', providers: [], template: ` <form [ngFormModel]="form"> <input type="text" phone [(ngModel)]="data" ngControl="phone"> </form> `, directives: [PhoneMask]})export class App { constructor(fb: FormBuilder) { this.form = fb.group({ phone: [''] }) }}
Angular 4+
I've created a generic directive, able to receive any mask and also able to define the mask dynamically based on the value:
mask.directive.ts:
import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';import { NgControl } from '@angular/forms';import { MaskGenerator } from '../interfaces/mask-generator.interface';@Directive({ selector: '[spMask]' })export class MaskDirective { private static readonly ALPHA = 'A'; private static readonly NUMERIC = '9'; private static readonly ALPHANUMERIC = '?'; private static readonly REGEX_MAP = new Map([ [MaskDirective.ALPHA, /\w/], [MaskDirective.NUMERIC, /\d/], [MaskDirective.ALPHANUMERIC, /\w|\d/], ]); private value: string = null; private displayValue: string = null; @Input('spMask') public maskGenerator: MaskGenerator; @Input('spKeepMask') public keepMask: boolean; @Input('spMaskValue') public set maskValue(value: string) { if (value !== this.value) { this.value = value; this.defineValue(); } }; @Output('spMaskValueChange') public changeEmitter = new EventEmitter<string>(); @HostListener('input', ['$event']) public onInput(event: { target: { value?: string }}): void { let target = event.target; let value = target.value; this.onValueChange(value); } constructor(private ngControl: NgControl) { } private updateValue(value: string) { this.value = value; this.changeEmitter.emit(value); MaskDirective.delay().then( () => this.ngControl.control.updateValueAndValidity() ); } private defineValue() { let value: string = this.value; let displayValue: string = null; if (this.maskGenerator) { let mask = this.maskGenerator.generateMask(value); if (value != null) { displayValue = MaskDirective.mask(value, mask); value = MaskDirective.processValue(displayValue, mask, this.keepMask); } } else { displayValue = this.value; } MaskDirective.delay().then(() => { if (this.displayValue !== displayValue) { this.displayValue = displayValue; this.ngControl.control.setValue(displayValue); return MaskDirective.delay(); } }).then(() => { if (value != this.value) { return this.updateValue(value); } }); } private onValueChange(newValue: string) { if (newValue !== this.displayValue) { let displayValue = newValue; let value = newValue; if ((newValue == null) || (newValue.trim() === '')) { value = null; } else if (this.maskGenerator) { let mask = this.maskGenerator.generateMask(newValue); displayValue = MaskDirective.mask(newValue, mask); value = MaskDirective.processValue(displayValue, mask, this.keepMask); } this.displayValue = displayValue; if (newValue !== displayValue) { this.ngControl.control.setValue(displayValue); } if (value !== this.value) { this.updateValue(value); } } } private static processValue(displayValue: string, mask: string, keepMask: boolean) { let value = keepMask ? displayValue : MaskDirective.unmask(displayValue, mask); return value } private static mask(value: string, mask: string): string { value = value.toString(); let len = value.length; let maskLen = mask.length; let pos = 0; let newValue = ''; for (let i = 0; i < Math.min(len, maskLen); i++) { let maskChar = mask.charAt(i); let newChar = value.charAt(pos); let regex: RegExp = MaskDirective.REGEX_MAP.get(maskChar); if (regex) { pos++; if (regex.test(newChar)) { newValue += newChar; } else { i--; len--; } } else { if (maskChar === newChar) { pos++; } else { len++; } newValue += maskChar; } } return newValue; } private static unmask(maskedValue: string, mask: string): string { let maskLen = (mask && mask.length) || 0; return maskedValue.split('').filter( (currChar, idx) => (idx < maskLen) && MaskDirective.REGEX_MAP.has(mask[idx]) ).join(''); } private static delay(ms: number = 0): Promise<void> { return new Promise(resolve => setTimeout(() => resolve(), ms)).then(() => null); }}
(Remember to declare it in your NgModule)
The numeric character in the mask is 9
so your mask would be (999) 999-9999
. You can change the NUMERIC
static field if you want (if you change it to 0
, your mask should be (000) 000-0000
, for example).
The value is displayed with mask but stored in the component field without mask (this is the desirable behaviour in my case). You can make it be stored with mask using [spKeepMask]="true"
.
The directive receives an object that implements the MaskGenerator
interface.
mask-generator.interface.ts:
export interface MaskGenerator { generateMask: (value: string) => string;}
This way it's possible to define the mask dynamically based on the value (like credit cards).
I've created an utilitarian class to store the masks, but you can specify it directly in your component too.
my-mask.util.ts:
export class MyMaskUtil { private static PHONE_SMALL = '(999) 999-9999'; private static PHONE_BIG = '(999) 9999-9999'; private static CPF = '999.999.999-99'; private static CNPJ = '99.999.999/9999-99'; public static PHONE_MASK_GENERATOR: MaskGenerator = { generateMask: () => MyMaskUtil.PHONE_SMALL, } public static DYNAMIC_PHONE_MASK_GENERATOR: MaskGenerator = { generateMask: (value: string) => { return MyMaskUtil.hasMoreDigits(value, MyMaskUtil.PHONE_SMALL) ? MyMaskUtil.PHONE_BIG : MyMaskUtil.PHONE_SMALL; }, } public static CPF_MASK_GENERATOR: MaskGenerator = { generateMask: () => MyMaskUtil.CPF, } public static CNPJ_MASK_GENERATOR: MaskGenerator = { generateMask: () => MyMaskUtil.CNPJ, } public static PERSON_MASK_GENERATOR: MaskGenerator = { generateMask: (value: string) => { return MyMaskUtil.hasMoreDigits(value, MyMaskUtil.CPF) ? MyMaskUtil.CNPJ : MyMaskUtil.CPF; }, } private static hasMoreDigits(v01: string, v02: string): boolean { let d01 = this.onlyDigits(v01); let d02 = this.onlyDigits(v02); let len01 = (d01 && d01.length) || 0; let len02 = (d02 && d02.length) || 0; let moreDigits = (len01 > len02); return moreDigits; } private static onlyDigits(value: string): string { let onlyDigits = (value != null) ? value.replace(/\D/g, '') : null; return onlyDigits; }}
Then you can use it in your component (use spMaskValue
instead of ngModel
, but if is not a reactive form, use ngModel
with nothing, like in the example below, just so that you won't receive an error of no provider because of the injected NgControl
in the directive; in reactive forms you don't need to include ngModel
):
my.component.ts:
@Component({ ... })export class MyComponent { public phoneValue01: string = '1231234567'; public phoneValue02: string; public phoneMask01 = MyMaskUtil.PHONE_MASK_GENERATOR; public phoneMask02 = MyMaskUtil.DYNAMIC_PHONE_MASK_GENERATOR;}
my.component.html:
<span>Phone 01 ({{ phoneValue01 }}):</span><br><input type="text" [(spMaskValue)]="phoneValue01" [spMask]="phoneMask01" ngModel><br><br><span>Phone 02 ({{ phoneValue02 }}):</span><br><input type="text" [(spMaskValue)]="phoneValue02" [spMask]="phoneMask02" [spKeepMask]="true" ngModel>
(Take a look at phone02
and see that when you type 1 more digit, the mask changes; also, look that the value stored of phone01
is without mask)
I've tested it with normal inputs as well as with ionic
inputs (ion-input
), with both reactive (with formControlName
, not with formControl
) and non-reactive forms.