Поля ввода маски в формах Angular2

Возможно ли иметь форму, управляемую моделью в Angular 2, и найти директиву, которая позволила бы маскировать поле ввода, например, номер телефона (123) 123-4567 ?

Plunker> = RC.5

оригинал

Один из способов сделать это – использовать директиву, которая вводит NgControl и управляет значением

( подробнее см. встроенные комментарии )

 @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: [''] }) } }

Пример плунжера <= RC.5

Я делаю это с помощью TextMaskModule из « angular2-text-mask »

Шахта разделена, но вы можете получить идею

Пакет с использованием NPM NodeJS

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

HTML

   

Внутренний компонент

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

Угловая 4+

Я создал общую директиву , способную получать любую маску, а также динамически определять маску на основе значения:

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

(Не забудьте объявить его в своем NgModule)

Числовой символ в маске равен 9 поэтому ваша маска будет (999) 999-9999 . Вы можете изменить статическое поле NUMERIC если хотите (если вы измените его на 0 , ваша маска должна быть (000) 000-0000 , например).

Значение отображается с маской, но сохраняется в поле компонента без маски (это желательное поведение в моем случае). Вы можете сохранить его в маске, используя [spKeepMask]="true" .

Директива получает объект, реализующий интерфейс MaskGenerator .

Маска-generator.interface.ts:

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

Таким образом можно динамически определить маску на основе значения (например, кредитных карт).

Я создал утилитарный class для хранения масок, но вы также можете указать его прямо в своем компоненте.

мой-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; } } 

Затем вы можете использовать его в своем компоненте (используйте spMaskValue вместо ngModel , но если это не реактивная форма, используйте ngModel без ничего, как в приведенном ниже примере, просто чтобы вы не получили ошибку ни одного провайдера из-за вводят NgControl в директиву, в реактивных формах вам не нужно включать 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 }}):

(Взгляните на phone02 и посмотрите, что, когда вы вводите еще 1 цифру, изменяется маска, а также посмотрите, что значение, сохраненное в phone01 , не имеет маски)

Я тестировал его с нормальными входами, а также с ionic входами ( ion-input ), причем оба реактивных (с formControlName , а не с formControl ) и formControl формами.

Это можно сделать с помощью директивы. Ниже находится плунжер входной маски, которую я построил.

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

Код:

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

}

Примечание. Есть еще несколько ошибок.

Объединив ответ Гюнтера Цохбауэра с добрым ванильным JS , вот директива с двумя линиями логики, поддерживающая формат (123) 456-7890 .

Реактивные формы: 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); } } 

Шаблонные формы : 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); } } 

Реактивная форма


Смотрите в Stackblitz

Дополнение к ответу @ Günter Zöchbauer выше, я попытался сделать следующее, и, похоже, он работает, но я не уверен, эффективен ли он.

Я использую valueChanges наблюдения за событиями изменений в реактивной форме, подписываясь на него. Для специальной обработки backspace я получаю data от подписки и проверяю ее с помощью userForm.value.phone(from [formGroup]="userForm") . Потому что в этот момент данные изменяются на новое значение, но последнее ссылается на предыдущее значение из-за отсутствия настройки. Если данные меньше предыдущего значения, пользователь должен удалить символ из ввода. В этом случае измените шаблон следующим образом:

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

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

В противном случае, как упоминал выше Günter Zöchbauer, удаление нечисловых символов не распознается, потому что, когда мы удаляем скобки из ввода, цифры остаются неизменными и снова добавляются круглые скобки из соответствия шаблону.

controller:

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

Шаблон:

 

ОБНОВИТЬ

Есть ли способ сохранить позицию курсора во время обратной перестановки в середине строки? В настоящее время он возвращается к концу.

Определите id и renderer2 # selectRootElement, чтобы получить собственный элемент в компоненте.

Таким образом, мы можем получить позицию курсора, используя:

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

и затем мы можем применить его после ввода нового значения:

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

ОБНОВЛЕНИЕ 2

Вероятно, лучше поставить такую ​​логику внутри директивы, а не в компонент

Я также поставил логику в директиву. Это упрощает применение к другим элементам.

Смотрите в Stackblitz

Примечание. Это характерно для шаблона (123) 123-4567 .

Не нужно изобретать велосипед! Используйте Currency Mask , в отличие от TextMaskModule , этот работает с типом ввода номера, и его очень легко настроить. Я обнаружил, что когда я сделал свою собственную директиву, мне пришлось продолжать преобразовывать число и строку в вычисления. Сэкономьте время. Вот ссылка:

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

  • Java проверяет, перекрываются ли два прямоугольника в любой точке
  • Производительность ASP.NET MVC
  • Отключить проверку элементов формы HTML5
  • jquery / js - Как выбрать родительскую форму, на основе которой нажата кнопка отправки?
  • MVC публикует список сложных объектов
  • Как вы создаете сообщения для проверки формы HTML5?
  • Какие символы разрешены в адресе электронной почты?
  • jqGrid - Как изменить конструкцию form_editing?
  • jQuery .val () vs .attr ("value")
  • Как читать данные в формате PDF с помощью iTextSharp?
  • Аккордеон в Windows Forms DataGridView
  • Давайте будем гением компьютера.