Campos de máscara de entrada en formularios Angular2

¿Es posible tener una forma impulsada por el modelo en Angular 2 y encontrar una directiva que permita enmascarar el campo de entrada como la entrada del número de teléfono (123) 123-4567 ?

    Plunker> = RC.5

    original

    Una forma de hacerlo es usar una directiva que inyecte NgControl y manipule el valor

    ( para detalles vea comentarios en línea )

     @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: ` 
    `, directives: [PhoneMask] }) export class App { constructor(fb: FormBuilder) { this.form = fb.group({ phone: [''] }) } }

    Ejemplo de Plunker < = RC.5

    Lo hago usando el TextMaskModule de ‘ angular2-text-mask

    Los míos están divididos pero puedes tener la idea

    Paquete usando NPM NodeJS

     "dependencies": { "angular2-text-mask": "8.0.0", 

    HTML

       

    Componente interior

     public areaCodeModel = ''; public areaCodeMask = ['(', /[1-9]/, /\d/, /\d/, ')']; public phoneModel = ''; public phoneMask = [/\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/]; 

    Angular 4+

    Creé una directiva genérica , capaz de recibir cualquier máscara y también poder definir la máscara de forma dinámica en función del valor:

    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(); @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 { return new Promise(resolve => setTimeout(() => resolve(), ms)).then(() => null); } } 

    (Recuerde declararlo en su NgModule)

    El carácter numérico en la máscara es 9 por lo que su máscara sería (999) 999-9999 . Puede cambiar el campo estático NUMERIC si lo desea (si lo cambia a 0 , su máscara debería ser (000) 000-0000 , por ejemplo).

    El valor se muestra con máscara, pero se almacena en el campo del componente sin máscara (este es el comportamiento deseable en mi caso). Puede hacer que se almacene con máscara usando [spKeepMask]="true" .

    La directiva recibe un objeto que implementa la interfaz MaskGenerator .

    mask-generator.interface.ts:

     export interface MaskGenerator { generateMask: (value: string) => string; } 

    De esta forma, es posible definir la máscara de forma dinámica en función del valor (como las tarjetas de crédito).

    Creé una clase utilitaria para almacenar las máscaras, pero también puedes especificarla directamente en tu componente.

    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; } } 

    Luego puede usarlo en su componente (use spMaskValue lugar de ngModel , pero si no es un formulario reactivo, use ngModel sin nada, como en el ejemplo a continuación, solo para que no reciba un error de ningún proveedor debido a la inyectó NgControl en la directiva, en formas reactivas no necesita incluir 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:

     Phone 01 ({{ phoneValue01 }}):


    Phone 02 ({{ phoneValue02 }}):

    (Eche un vistazo al phone02 y observe que cuando escribe 1 dígito más, la máscara cambia; también, observe que el valor almacenado del phone01 tiene máscara)

    Lo he probado con entradas normales y con entradas ionic ( ion-input ), con reactivo (con formControlName , no con formControl ) y formas no reactivas.

    Se puede hacer usando una directiva. Debajo está el plunker de la máscara de entrada que construí.

    https://plnkr.co/edit/hRsmd0EKci6rjGmnYFRr?p=preview

    Código:

     import {Directive, Attribute, ElementRef, OnInit, OnChanges, Input, SimpleChange } from 'angular2/core'; import {NgControl, DefaultValueAccessor} from 'angular2/common'; @Directive({ selector: '[mask-input]', host: { //'(keyup)': 'onInputChange()', '(click)': 'setInitialCaretPosition()' }, inputs: ['modify'], providers: [DefaultValueAccessor] }) export class MaskDirective implements OnChanges { maskPattern: string; placeHolderCounts: any; dividers: string[]; modelValue: string; viewValue: string; intialCaretPos: any; numOfChar: any; @Input() modify: any; constructor(public model: NgControl, public ele: ElementRef, @Attribute("mask-input") maskPattern: string) { this.dividers = maskPattern.replace(/\*/g, "").split(""); this.dividers.push("_"); this.generatePattern(maskPattern); this.numOfChar = 0; } ngOnChanges(changes: { [propertyName: string]: SimpleChange }) { this.onInputChange(changes); } onInputChange(changes: { [propertyName: string]: SimpleChange }) { this.modelValue = this.getModelValue(); var caretPosition = this.ele.nativeElement.selectionStart; if (this.viewValue != null) { this.numOfChar = this.getNumberOfChar(caretPosition); } var stringToFormat = this.modelValue; if (stringToFormat.length < 10) { stringToFormat = this.padString(stringToFormat); } this.viewValue = this.format(stringToFormat); if (this.viewValue != null) { caretPosition = this.setCaretPosition(this.numOfChar); } this.model.viewToModelUpdate(this.modelValue); this.model.valueAccessor.writeValue(this.viewValue); this.ele.nativeElement.selectionStart = caretPosition; this.ele.nativeElement.selectionEnd = caretPosition; } generatePattern(patternString) { this.placeHolderCounts = (patternString.match(/\*/g) || []).length; for (var i = 0; i < this.placeHolderCounts; i++) { patternString = patternString.replace('*', "{" + i + "}"); } this.maskPattern = patternString; } format(s) { var formattedString = this.maskPattern; for (var i = 0; i < this.placeHolderCounts; i++) { formattedString = formattedString.replace("{" + i + "}", s.charAt(i)); } return formattedString; } padString(s) { var pad = "__________"; return (s + pad).substring(0, pad.length); } getModelValue() { var modelValue = this.model.value; if (modelValue == null) { return ""; } for (var i = 0; i < this.dividers.length; i++) { while (modelValue.indexOf(this.dividers[i]) > -1) { modelValue = modelValue.replace(this.dividers[i], ""); } } return modelValue; } setInitialCaretPosition() { var caretPosition = this.setCaretPosition(this.modelValue.length); this.ele.nativeElement.selectionStart = caretPosition; this.ele.nativeElement.selectionEnd = caretPosition; } setCaretPosition(num) { var notDivider = true; var caretPos = 1; for (; num > 0; caretPos++) { var ch = this.viewValue.charAt(caretPos); if (!this.isDivider(ch)) { num--; } } return caretPos; } isDivider(ch) { for (var i = 0; i < this.dividers.length; i++) { if (ch == this.dividers[i]) { return true; } } } getNumberOfChar(pos) { var num = 0; var containDividers = false; for (var i = 0; i < pos; i++) { var ch = this.modify.charAt(i); if (!this.isDivider(ch)) { num++; } else { containDividers = true; } } if (containDividers) { return num; } else { return this.numOfChar; } } 

    }

    Nota: todavía hay algunos errores.

    Combinando la respuesta de Günter Zöchbauer con el buen viejo vanilla-JS , aquí hay una directiva con dos líneas de lógica que admite el formato (123) 456-7890 .

    Formas reactivas: Plunk

     import { Directive, Output, EventEmitter } from "@angular/core"; import { NgControl } from "@angular/forms"; @Directive({ selector: '[formControlName][phone]', host: { '(ngModelChange)': 'onInputChange($event)' } }) export class PhoneMaskDirective { @Output() rawChange:EventEmitter = new EventEmitter(); constructor(public model: NgControl) {} onInputChange(value) { var x = value.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/); var y = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : ''); this.model.valueAccessor.writeValue(y); this.rawChange.emit(rawValue); } } 

    Formularios basados ​​en plantillas : Plunk

     import { Directive } from "@angular/core"; import { NgControl } from "@angular/forms"; @Directive({ selector: '[ngModel][phone]', host: { '(ngModelChange)': 'onInputChange($event)' } }) export class PhoneMaskDirective { constructor(public model: NgControl) {} onInputChange(value) { var x = value.replace(/\D/g, '').match(/(\d{0,3})(\d{0,3})(\d{0,4})/); value = !x[2] ? x[1] : '(' + x[1] + ') ' + x[2] + (x[3] ? '-' + x[3] : ''); this.model.valueAccessor.writeValue(value); } } 

    Forma reactiva


    Ver en Stackblitz

    Además de la respuesta anterior de @Günter Zöchbauer , intenté lo siguiente y parece estar funcionando, pero no estoy seguro de si es una manera eficiente.

    Uso valueChanges observable para escuchar eventos de cambio en la forma reactiva suscribiéndome a él. Para el manejo especial del retroceso, obtengo los data de la suscripción y lo compruebo con userForm.value.phone(from [formGroup]="userForm") . Porque, en ese momento, los datos cambian al nuevo valor, pero este último se refiere al valor anterior debido a que aún no se ha establecido. Si los datos son menores que el valor anterior, entonces el usuario debe eliminar el carácter de la entrada. En este caso, cambie el patrón de la siguiente manera:

    from: newVal = newVal.replace(/^(\d{0,3})/, '($1)');

    a: newVal = newVal.replace(/^(\d{0,3})/, '($1');

    De lo contrario, como Günter Zöchbauer mencionó anteriormente, no se reconoce la eliminación de caracteres no numéricos porque cuando eliminamos paréntesis de la entrada, los dígitos siguen siendo los mismos y se vuelven a agregar paréntesis del patrón.

    Controlador:

     import { Component,OnInit } from '@angular/core'; import { FormGroup,FormBuilder,AbstractControl,Validators } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit{ constructor(private fb:FormBuilder) { this.createForm(); } createForm(){ this.userForm = this.fb.group({ phone:['',[Validators.pattern(/^\(\d{3}\)\s\d{3}-\d{4}$/),Validators.required]], }); } ngOnInit() { this.phoneValidate(); } phoneValidate(){ const phoneControl:AbstractControl = this.userForm.controls['phone']; phoneControl.valueChanges.subscribe(data => { /**the most of code from @Günter Zöchbauer's answer.*/ /**we remove from input but: @preInputValue still keep the previous value because of not setting. */ let preInputValue:string = this.userForm.value.phone; let lastChar:string = preInputValue.substr(preInputValue.length - 1); var newVal = data.replace(/\D/g, ''); //when removed value from input if (data.length < preInputValue.length) { /**while removing if we encounter ) character, then remove the last digit too.*/ if(lastChar == ')'){ newVal = newVal.substr(0,newVal.length-1); } if (newVal.length == 0) { newVal = ''; } else if (newVal.length <= 3) { /**when removing, we change pattern match. "otherwise deleting of non-numeric characters is not recognized"*/ 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'); } //when typed value in input } else{ if (newVal.length == 0) { newVal = ''; } 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'); } } this.userForm.controls['phone'].setValue(newVal,{emitEvent: false}); }); } } 

    Modelo:

     

    ACTUALIZAR

    ¿Hay alguna manera de conservar la posición del cursor mientras retrocede en el medio de la cuerda? Actualmente, salta de nuevo al final.

    Defina una identificación y renderer2 # selectRootElement para obtener el elemento nativo en el componente.

    Entonces podemos obtener la posición del cursor usando:

     let start = this.renderer.selectRootElement('#tel').selectionStart; let end = this.renderer.selectRootElement('#tel').selectionEnd; 

    y luego podemos aplicarlo después de que la entrada se actualice a un nuevo valor:

     this.userForm.controls['phone'].setValue(newVal,{emitEvent: false}); //keep cursor the appropriate position after setting the input above. this.renderer.selectRootElement('#tel').setSelectionRange(start,end); 

    ACTUALIZACIÓN 2

    Probablemente sea mejor poner este tipo de lógica dentro de una directiva en lugar de en el componente

    También puse la lógica en una directiva. Esto hace que sea más fácil aplicarlo a otros elementos.

    Ver en Stackblitz

    Nota: Es específico para el patrón (123) 123-4567 .

    ¡No hay necesidad de reinventar la rueda! Use Currency Mask , a diferencia de TextMaskModule , este funciona con el tipo de entrada numérica y es muy fácil de configurar. Cuando hice mi propia directiva descubrí que tenía que seguir convirtiendo el número y la cadena para hacer cálculos. Ahórrate el tiempo. Aquí está el enlace:

    https://github.com/cesarrew/ng2-currency-mask