屏蔽输入以允许电话号码?

有没有可能在角度2中有模型驱动的形式,并实现一个指令,允许像电话号码条目 (123) 123-4567一样屏蔽一个 input字段?

187845 次浏览

Angular5和 Angular6:

角度5和6推荐的方法是使用@HostBindings 和@HostListener 而不是 host 属性

删除主机并添加 @ HostListener

 @HostListener('ngModelChange', ['$event'])
onModelChange(event) {
this.onInputChange(event, false);
}


@HostListener('keydown.backspace', ['$event'])
keydownBackspace(event) {
this.onInputChange(event.target.value, true);
}

工作在线堆栈闪电战链接: https://angular6-phone-mask.stackblitz.io

Stackblitz 代码示例: https://stackblitz.com/edit/angular6-phone-mask

官方文件链接

角2和角4:

普朗克 > = 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: `
<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: ['']
})
}
}

普朗克的例子 < = RC.5

它可以使用一个指令来完成。下面是我构建的输入掩码的活塞。

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

}

注意: 还是有一些 bug。

我使用来自‘ 角度2-文本掩码’的 TextMaskModule来完成这个操作

我的是分开的,但你可以得到的想法

使用 NPM NodeJS 的包

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

超文本标示语言

<input *ngIf="column?.type =='areaCode'" type="text" [textMask]="{mask: areaCodeMask}" [(ngModel)]="areaCodeModel">




<input *ngIf="column?.type =='phone'" type="text" [textMask]="{mask: phoneMask}" [(ngModel)]="phoneModel">

内部组件

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


public phoneModel = '';
public phoneMask = [/\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/];

没有必要重新发明轮子!与 TextMaskModule不同,使用 货币面具可以处理数字输入类型,而且非常容易配置。我发现当我做自己的指令时,我必须不断地在数字和字符串之间进行转换来进行计算。别浪费时间了。链接如下:

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

角度4 +

我已经创建了一个 通用指令,能够 接受任何面具,也能够 动态定义掩码的基础上的价值:

指令:

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

(记得在 NgModule 中声明它)

掩码中的数字字符是 9,因此您的掩码将是 (999) 999-9999。如果需要,可以更改 NUMERIC静态字段(例如,如果更改为 0,则掩码应为 (000) 000-0000)。

这个值用掩码显示,但是存储在没有掩码的组件字段中(在我的例子中,这是理想的行为)。您可以使用 [spKeepMask]="true"使它与掩码一起存储。

该指令接收一个实现 MaskGenerator接口的对象。

掩模发生器接口:

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

这样就可以根据价值(如信用卡)来计算 动态定义掩码

我已经创建了一个实用类来存储掩码,但是您也可以直接在组件中指定它。

效用:

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

组件:

@Component({ ... })
export class MyComponent {
public phoneValue01: string = '1231234567';
public phoneValue02: string;
public phoneMask01 = MyMaskUtil.PHONE_MASK_GENERATOR;
public phoneMask02 = MyMaskUtil.DYNAMIC_PHONE_MASK_GENERATOR;
}

返回文章页面

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

(看一下 phone02,当你多输入1个数字时,掩码就会改变; 同时,看一下 phone01存储的值没有掩码)

我已经用正常输入和 ionic输入(ion-input)测试了它,既有反应型(formControlName,而不是 formControl)也有非反应型。

活动表格


看看 Stackblitz

除了上面 @ G ünter Zöchbauer的答案之外,我尝试了以下方法,它似乎是有效的,但我不确定它是否是一种有效的方法。

我使用 valueChanges通过订阅来监听反应形式的更改事件。对于退格的特殊处理,我从订阅中获得 data并用 userForm.value.phone(from [formGroup]="userForm")检查它。因为在这个时候,数据变成了新值,但是后者引用了前一个值,因为还没有设置。如果数据小于以前的值,那么用户应该从输入中删除字符。在这种情况下,更改模式如下:

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

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

否则,正如 Günter Zöchbauer 上面提到的,非数字字符的删除是无法识别的,因为当我们从输入中删除括号时,数字仍然保持不变,并从模式匹配中再次添加括号。

总监:

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


}

模板:

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

更新

有没有一种方法可以保持光标的位置,同时在字符串中间的后退间距?目前,它跳回到结束。

定义 id <input id="tel" formControlName="phone" #phoneRef>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模式 的。

将 Günter Zöchbauer 的答案与 老香草 JS结合起来,这里有一个指令,它有两行支持 (123-456-7890)格式的逻辑。

Reactive Forms: 反应形式: < a href = “ https://plnkr.co/edit/KGWk0iAm2I45pnF68Ar8”rel = “ nofollow noReferrer”> 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);
}
}

模板驱动的窗体 : < a href = “ https://plnkr.co/edit/QxU8l3vHw8BZhrzbSDqJ”rel = “ nofollow noReferrer”> 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);
}
}

你可以使用 Cleave.js

// phone (123) 123-4567
var cleavePhone = new Cleave('.input-phone', {
//prefix: '(123)',
delimiters: ['(',') ','-'],
blocks: [0, 3, 3, 4]
});

演示: https://jsfiddle.net/emirM/a8fogse1/

我认为最简单的解决方案是添加 面具

npm i --save ngx-mask

然后你就可以

<input type='text' mask='(000) 000-0000' >

或者

<p>\{\{ phoneVar | mask: '(000) 000-0000' }} </p>
<input type="text" formControlName="gsm" (input)="formatGsm($event.target.value)">


formatGsm(inputValue: String): String {
const value = inputValue.replace(/[^0-9]/g, ''); // remove except digits
let format = '(***) *** ** **'; // You can change format


for (let i = 0; i < value.length; i++) {
format = format.replace('*', value.charAt(i));
}


if (format.indexOf('*') >= 0) {
format = format.substring(0, format.indexOf('*'));
}


return format.trim();
}

导出类 MyMaskUtil {

public static MASK_CEP(value: string):string {
return value = value.replace(/^(\d{5})(\d{3}).*/, '$1-$2');
}


public static MASK_TELEFONE(value: string):string {
return value = value.replace(/^(\d{2})(\d{5})(\d{4}).*/, '($1)$2-$3');
}


public static MASK_CPF(value: string):string {
return value = value.replace(/^(\d{3})^(\d{3})^(\d{3})(\d{2}).*/, '$1.$2.$3-$4');
}

}

RECEBA

信贷埃尼尔森菲利欧