输入掩码

时间:2016-06-13 23:45:59

标签: angular forms input

是否可以在Angular 2中使用模型驱动的表单并实现一个允许屏蔽input字段的指令,如电话号码条目(123) 123-4567

10 个答案:

答案 0 :(得分:56)

Angular5和6:

angular 5和6推荐的方法是使用@HostBindings和@HostListeners而不是主机属性

删除主机并添加 @HostListener

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

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

在线工作stackblitz链接:https://angular6-phone-mask.stackblitz.io

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

官方文档链接https://angular.io/guide/attribute-directives#respond-to-user-initiated-events

Angular2和4:

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

Plunker example <= RC.5

答案 1 :(得分:12)

Angular 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<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接口的对象。

掩模generator.interface.ts:

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

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

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

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

然后您可以在您的组件中使用它(使用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:

<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)和非反应形式。

答案 2 :(得分:9)

我使用“ angular2-text-mask

中的 TextMaskModule 执行此操作

我的分裂,但你可以得到这个想法

使用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"> 

内部组件

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

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

答案 3 :(得分:6)

反应表格

<强> See at Stackblitz

除了 @GünterZöchbauer上面的答案之外,我尝试了如下,它似乎正在运作,但我不确定它是否是一种有效的方法。

我使用valueChanges observable通过订阅来监听被动形式的变更事件。对于退格的特殊处理,我从订阅中获取data并使用userForm.value.phone(from [formGroup]="userForm")进行检查。因为,在那一刻,数据变为新值,但后者由于尚未设置而引用之前的值。如果数据小于先前值,则用户应从输入中删除字符。在这种情况下,更改模式如下:

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

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

否则,正如GünterZö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

  

将这种逻辑放在指令中可能更好   而不是在组件中

我还将逻辑放入指令中。这样可以更轻松地将其应用于其他元素。

<强> See at Stackblitz

注意: 特定于(123) 123-4567模式

答案 4 :(得分:4)

可以使用指令完成。下面是我构建的输入掩码的plunker。

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

}

注意:仍有一些错误。

答案 5 :(得分:2)

将GünterZöchbauer的答案与good-old vanilla-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<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); 
  }
}

模板驱动的表单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);       
  }
}

答案 6 :(得分:0)

无需重新发明轮子!使用货币掩码,与 TextMaskModule 不同,这个使用数字输入类型,并且它非常容易配置。我发现当我制作自己的指令时,我不得不在数字和字符串之间进行转换以进行计算。节省时间。这是链接:

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

答案 7 :(得分:0)

您可以使用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/

答案 8 :(得分:0)

我认为最简单的解决方案是添加ngx-mask

npm i --save ngx-mask

那你就可以做

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

OR

<p>{{ phoneVar | mask: '(000) 000-0000' }} </p>

答案 9 :(得分:0)

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