web-dev-qa-db-fra.com

Masque pour une entrée

Est-il possible d'avoir un formulaire basé sur un modèle dans Angular 2 et d'implémenter une directive permettant de masquer un champ input comme une entrée de numéro de téléphone (123) 123-4567

33
bensiu

Angular5 et 6:

façon angulaire 5 et 6 consiste à utiliser @HostBindings et @HostListeners au lieu de la propriété Host

supprimer l'hôte et ajouter @HostListener

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

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

Travailler en ligne stackblitz Link: https://angular6-phone-mask.stackblitz.io

Exemple de code Stackblitz: https://stackblitz.com/edit/angular6-phone-mask

Lien à la documentation officielle https://angular.io/guide/attribute-directives#respond-to-user-initiated-events

Angular2 et 4:

Plunker> = RC.5

original

Une façon de le faire consiste à utiliser une directive qui injecte NgControl et manipule la valeur

(pour plus de détails, voir les commentaires en ligne)

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

exemple Plunker <= RC.5

43
Günter Zöchbauer

Angulaire 4+

J'ai créé une directive générique, capable de recevoir tout masque et également capable de définir le masque de manière dynamique en fonction de la valeur:

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

(N'oubliez pas de le déclarer dans votre NgModule)

Le caractère numérique dans le masque est 9 et votre masque serait donc (999) 999-9999. Vous pouvez modifier le champ statique NUMERIC si vous le souhaitez (par exemple, si vous le modifiez en 0, votre masque devrait être (000) 000-0000).

La valeur est affichée avec masque mais stockée dans le champ composant sans masque (il s'agit du comportement souhaité dans mon cas). Vous pouvez le stocker avec masque en utilisant [spKeepMask]="true".

La directive reçoit un objet qui implémente l'interface MaskGenerator.

mask-generator.interface.ts:

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

De cette façon, il est possible de définir le masque de manière dynamique en fonction de la valeur (comme les cartes de crédit).

J'ai créé une classe utilitaire pour stocker les masques, mais vous pouvez aussi le spécifier directement dans votre composant.

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

Ensuite, vous pouvez l’utiliser dans votre composant (utilisez spMaskValue au lieu de ngModel, mais si ce n’est pas un formulaire réactif, utilisez ngModel avec rien, comme dans l’exemple ci-dessous, afin que vous ne receviez pas d’erreur de fournisseur à cause du Injecté NgControl dans la directive; dans les formulaires réactifs, vous n'avez pas besoin d'inclure 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>

(Regardez phone02 et voyez que lorsque vous tapez 1 chiffre supplémentaire, le masque change; regardez aussi que la valeur stockée de phone01 est sans masque)

Je l'ai testé avec des entrées normales ainsi qu'avec des entrées ionic (ion-input), avec des formulaires réactifs (avec formControlName, pas avec formControl) et non réactifs.

10
Lucas Basquerotto

Je le fais en utilisant le TextMaskModule from ' angular2-text-mask '

Les miens sont divisés mais tu peux avoir l'idée

Paquet utilisant 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"> 

Composant intérieur

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

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

Forme réactive


Voir à Stackblitz

Ajout à la réponse de @ Günter Zöchbauer ci-dessus, j'ai essayé comme suit et cela semble fonctionner, mais je ne suis pas sûr que ce soit un moyen efficace. 

J'utilise valueChanges observable pour écouter les événements de changement sous forme réactive en m'y abonnant. Pour un traitement spécial du retour arrière, je reçois la data de subscribe et le vérifie avec userForm.value.phone(from [formGroup]="userForm"). Parce que, à ce moment, les données changent à la nouvelle valeur mais celle-ci fait référence à la valeur précédente parce qu’elle n’a pas encore été définie Si les données sont inférieures à la valeur précédente, l'utilisateur doit supprimer le caractère de l'entrée. Dans ce cas, modifiez le modèle comme suit:

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

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

Autrement, comme l'a mentionné Günter Zöchbauer, la suppression des caractères non numériques n'est pas reconnue, car lorsque nous supprimons les parenthèses de l'entrée, les chiffres restent identiques et ajoutent à nouveau des parenthèses à partir du motif.

Manette:

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

}

Modèle:

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

METTRE &AGRAVE; JOUR

Existe-t-il un moyen de conserver la position du curseur en cas d'espacement arrière au milieu de la chaîne? Actuellement, ça revient à la fin.

Définissez un id <input id="tel" formControlName="phone" #phoneRef> Et renderer2 # selectRootElement pour obtenir le élément natif du composant.

Nous pouvons donc obtenir la position du curseur en utilisant: 

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

et nous pouvons ensuite l'appliquer après la mise à jour de l'entrée avec la nouvelle valeur:

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

UPDATE 2

Il est probablement préférable de mettre ce type de logique dans une directive plutôt que dans le composant

J'ai aussi mis la logique dans une directive. Cela facilite son application à d'autres éléments.

Voir à Stackblitz

Remarque: Il est spécifique au modèle (123) 123-4567.

6
Gurkan Yesilyurt

Cela peut être fait en utilisant une directive. Ci-dessous le plunker du masque de saisie que j'ai construit. 

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

Code:

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

}

Note: il y a encore quelques bugs.

4
Kyarie

En combinant la réponse de Günter Zöchbauer avec la vieille vieille Vanilla-JS , voici une directive avec deux lignes de logique prenant en charge le format (123) 456-7890.

Formes réactives: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); 
  }
}

Formulaires basés sur des modèles: 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);       
  }
}
2
Uğur Dinç

Pas besoin de réinventer la roue! Utilisez Currency Mask, contrairement à TextMaskModule, celui-ci fonctionne avec un type de saisie numérique et est très facile à configurer. J'ai constaté que lorsque je formulais ma propre directive, je devais continuer à convertir entre nombre et chaîne pour effectuer des calculs. Économise du temps. Voici le lien:

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

0
Mike Axle

vous pouvez utiliser cleave.js

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

démo: https://jsfiddle.net/emirM/a8fogse1/

0
Emir Mamashov

Je pense que la solution la plus simple consiste à ajouter ngx-mask

npm i --save ngx-mask

alors vous pouvez faire

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

OR

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