angular - tamaño - ¿Máscara para una entrada para permitir números de teléfono?
tamaño de formulario html (9)
Angular 4+
Creé una directiva genérica , capaz de recibir cualquier máscara y también capaz de definir la máscara dinámicamente 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<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);
}
}
(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 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 manera, es posible definir la máscara dinámicamente en función del valor (como las tarjetas de crédito).
He creado una clase utilitaria para almacenar las máscaras, pero también puede especificarla directamente en su 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 una forma reactiva, use
ngModel
sin nada, como en el ejemplo a continuación, solo para que no reciba un error de ningún proveedor debido al
NgControl
inyectado 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:
<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>
(Eche un vistazo a
phone02
y vea que cuando escribe 1 dígito más, la máscara cambia; también, observe que el valor almacenado de
phone01
es sin máscara)
Lo probé con entradas normales, así como con entradas
ionic
(
ion-input
), tanto con
formControlName
reactivas (con
formControlName
, no con
formControl
) como no reactivas.
¿Es posible tener una forma basada en modelos en Angular 2 e implementar una directiva que permita enmascarar un campo de
input
como una entrada de número de teléfono
(123) 123-4567
?
Forma reactiva
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 forma eficiente.
Utilizo
valueChanges
observable para escuchar eventos de cambio en forma reactiva suscribiéndome a él.
Para un manejo especial del retroceso, obtengo los
data
de la suscripción y los
userForm.value.phone(from [formGroup]="userForm")
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 configurado.
Si los datos son inferiores al valor anterior, 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 agregan nuevamente paréntesis de la coincidencia de patrones.
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:
<form [formGroup]="userForm" novalidate>
<div class="form-group">
<label for="tel">Tel:</label>
<input id="tel" formControlName="phone" maxlength="14">
</div>
<button [disabled]="userForm.status == ''INVALID''" type="submit">Send</button>
</form>
ACTUALIZAR
¿Hay alguna manera de preservar la posición del cursor mientras retrocede en el medio de la cadena? Actualmente, salta de regreso al final.
Defina un id
<input id="tel" formControlName="phone" #phoneRef>
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 al 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 facilita su aplicación a otros elementos.
Nota:
es específico del 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 de número y es muy fácil de configurar. Cuando hice mi propia directiva, tuve que seguir convirtiendo entre número y cadena para hacer cálculos. Ahórrate el tiempo. Aquí está el enlace:
Combinando la respuesta de Günter Zöchbauer con el 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<string> = new EventEmitter<string>();
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);
}
}
Formas basadas 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);
}
}
Creo que la solución más simple es agregar ngx-mask
npm i --save ngx-mask
entonces puedes hacer
<input type=''text'' mask=''(000) 000-0000'' >
O
<p>{{ phoneVar | mask: ''(000) 000-0000'' }} </p>
Hago esto usando TextMaskModule de '' angular2-text-mask ''
Los míos están divididos pero puedes entender la idea
Paquete usando NPM NodeJS
"dependencies": {
"angular2-text-mask": "8.0.0",
HTML
<input *ngIf="column?.type ==''areaCode''" type="text" [textMask]="{mask: areaCodeMask}" [(ngModel)]="areaCodeModel">
<input *ngIf="column?.type ==''phone''" type="text" [textMask]="{mask: phoneMask}" [(ngModel)]="phoneModel">
Componente interior
public areaCodeModel = '''';
public areaCodeMask = [''('', /[1-9]/, //d/, //d/, '')''];
public phoneModel = '''';
public phoneMask = [//d/, //d/, //d/, ''-'', //d/, //d/, //d/, //d/];
Se puede hacer usando una directiva. A continuación se muestra 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.
puedes usar cleave.js
// phone (123) 123-4567
var cleavePhone = new Cleave(''.input-phone'', {
//prefix: ''(123)'',
delimiters: [''('','') '',''-''],
blocks: [0, 3, 3, 4]
});
demostración: https://jsfiddle.net/emirM/a8fogse1/
Angular5 y 6:
la forma recomendada angular 5 y 6 es usar @HostBindings y @HostListeners en lugar de la propiedad del host
eliminar host y agregar @HostListener
@HostListener(''ngModelChange'', [''$event''])
onModelChange(event) {
this.onInputChange(event, false);
}
@HostListener(''keydown.backspace'', [''$event''])
keydownBackspace(event) {
this.onInputChange(event.target.value, true);
}
Trabajando en línea stackblitz Link: https://angular6-phone-mask.stackblitz.io
Ejemplo de código de Stackblitz: https://stackblitz.com/edit/angular6-phone-mask
Enlace de documentación oficial https://angular.io/guide/attribute-directives#respond-to-user-initiated-events
Angular2 y 4:
original
Una forma de hacerlo es usando una directiva que inyecta
NgControl
y manipula el valor
( para más detalles ver 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: `
<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: ['''']
})
}
}