69 votes

Comment utiliser les tuyaux dans la saisie réactive des formulaires d'Angular 5 ?

J'essaie de comprendre comment utiliser un pipe dans un formulaire réactif pour que la saisie soit forcée dans un format monétaire. J'ai déjà créé mon propre tube pour cela, que j'ai testé dans d'autres parties du code, et je sais donc qu'il fonctionne comme un simple tube. Le nom de mon tube est 'udpCurrency'.

La réponse la plus proche que j'ai pu trouver sur stack overflow était celle-ci : Utilisation de tuyaux dans ngModel sur des éléments INPUT dans Angular2-View Cependant, cela ne fonctionne pas dans mon cas et je pense que cela a quelque chose à voir avec le fait que mon formulaire est réactif.

Voici tout le code pertinent :

Le modèle

<form [formGroup]="myForm" #f="ngForm">
  <input class="form-control col-md-6" 
    formControlName="amount" 
    [ngModel]="f.value.amount | udpCurrency" 
    (ngModelChange)="f.value.amount=$event" 
    placeholder="Amount">
</form>

Le composant

import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(
    private builder: FormBuilder
  ) {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });
  }    
}

L'erreur :

ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined: '. Current value: 'undefined: undefined'

0 votes

L'erreur d'expression provient-elle du tuyau ou du composant ?

0 votes

Le composant html. en particulier la ligne qui a ceci "[ngModel]="f1.value.amount | udpCurrency"

0 votes

112voto

AJT_82 Points 30605

C'est ce qui peut arriver quand on mélange un formulaire piloté par un modèle et un formulaire réactif. Vous avez deux liaisons qui se combattent l'une l'autre. Choisissez soit le formulaire piloté par un modèle, soit le formulaire réactif. Si vous souhaitez opter pour un formulaire réactif, vous pouvez utiliser la méthode suivante [value] pour votre pipe...

Notez que ce tube ne sert qu'à afficher la sortie souhaitée dans la vue.

<form [formGroup]="myForm">
  <input 
    [value]="myForm.get('amount').value | udpCurrency"
    formControlName="amount" 
    placeholder="Amount">
</form>

2 votes

C'était extrêmement utile, merci. J'ai trouvé un tas d'articles sur ce sujet qui étaient tous excessivement complexes, mais celui-ci fonctionne tout simplement.

4 votes

Cela fonctionne initialement mais lorsque vous modifiez la valeur, le tuyau ne réapplique pas onblur. Est-ce quelque chose qui doit être géré explicitement ?

0 votes

@JacobRoberts Désolé, je n'ai pas vu votre commentaire depuis quelques mois. J'ai finalement compris ce que vous disiez et vous avez raison. J'ai une nouvelle réponse ci-dessous qui pourrait vous aider si vous rencontrez toujours le même problème.

21voto

Dallas Caley Points 164

Je pensais que cela fonctionnait mais il s'avère que j'avais tort (et que j'ai accepté une mauvaise réponse). J'ai juste refait ma logique d'une nouvelle manière qui fonctionne mieux pour moi et répond à la préoccupation de Jacob Roberts dans les commentaires ci-dessus. Voici ma nouvelle solution :

Le modèle :

<form [formGroup]="myForm">
  <input formControlName="amount" placeholder="Amount">
</form>

Le composant :

import { Component, OnInit, HostListener } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { UdpCurrencyMaskPipe } from '../../../_helpers/udp-currency-mask.pipe';

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(
    private builder: FormBuilder,
    private currencyMask: UdpCurrencyMaskPipe,
  ) {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });

    this.myForm.valueChanges.subscribe(val => {
      if (typeof val.amount === 'string') {
        const maskedVal = this.currencyMask.transform(val.amount);
        if (val.amount !== maskedVal) {
          this.myForm.patchValue({amount: maskedVal});
        }
      }
    });
  }    
}

Le Pipe :

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
    name: 'udpCurrencyMask'
})
export class UdpCurrencyMaskPipe implements PipeTransform {
    amount: any;

    transform(value: any, args?: any): any {

        let amount = String(value);

        const beforePoint = amount.split('.')[0];
        let integers = '';
        if (typeof beforePoint !== 'undefined') {
            integers = beforePoint.replace(/\D+/g, '');
        }
        const afterPoint = amount.split('.')[1];
        let decimals = '';
        if (typeof afterPoint !== 'undefined') {
            decimals = afterPoint.replace(/\D+/g, '');
        }
        if (decimals.length > 2) {
            decimals = decimals.slice(0, 2);
        }
        amount = integers;
        if (typeof afterPoint === 'string') {
            amount += '.';
        }
        if (decimals.length > 0) {
            amount += decimals;
        }

        return amount;
    }
}

Maintenant, il y a plusieurs choses que j'ai apprises ici. La première est que ce que Jacob a dit était vrai, l'autre méthode ne fonctionnait qu'au début mais ne se mettait pas à jour lorsque la valeur avait changé. Une autre chose très importante à noter est que j'ai besoin d'un type de tube complètement différent pour un masque par rapport à un tube de vue. Par exemple, un tube dans une vue pourrait prendre cette valeur "100" et la convertir en "$100.00", mais vous ne voudriez pas que cette conversion se produise pendant que vous tapez la valeur, vous voudriez qu'elle se produise seulement après que vous ayez fini de taper. C'est pour cette raison que j'ai créé mon pipe currency mask qui supprime simplement les nombres non numériques et limite la décimale à deux positions.

7voto

Drew Points 160

J'avais l'intention d'écrire un contrôle personnalisé, mais j'ai constaté que le fait de remplacer la commande "onChange" de la classe FormControl par la commande ngModelChange était plus facile. Le site emitViewToModelChange: false est essentiel dans votre logique de mise à jour pour éviter les boucles récurrentes d'événements de changement. Tout le piping vers la monnaie se fait dans le composant et vous n'avez pas à vous soucier des erreurs de la console.

<input matInput placeholder="Amount" 
  (ngModelChange)="onChange($event)" formControlName="amount" />

@Component({
  providers: [CurrencyPipe]
})
export class MyComponent {
  form = new FormGroup({
    amount: new FormControl('', { validators: Validators.required, updateOn: 'blur' })
  });

  constructor(private currPipe:CurrencyPipe) {}

  onChange(value:string) {
    const ctrl = this.form.get('amount') as FormControl;

    if(isNaN(<any>value.charAt(0))) {
      const val = coerceNumberProperty(value.slice(1, value.length));
      ctrl.setValue(this.currPipe.transform(val), { emitEvent: false, emitViewToModelChange: false });
    } else {
      ctrl.setValue(this.currPipe.transform(value), { emitEvent: false, emitViewToModelChange: false });
    }
  }

  onSubmit() {
    const rawValue = this.form.get('amount').value;

    // if you need to strip the '$' from your input's value when you save data
    const value = rawValue.slice(1, rawValue.length);

    // do whatever you need to with your value
  }
}

1voto

inorganik Points 2591

Les autres réponses ici n'ont pas fonctionné correctement pour moi mais j'ai trouvé un moyen qui fonctionne très bien. Vous devez appliquer la transformation pipe à l'intérieur de la souscription valueChanges du formulaire réactif, mais n'émettez pas l'événement pour ne pas créer une boucle récursive :

this.formGroup.valueChanges.subscribe(form => {
  if (form.amount) {
    this.formGroup.patchValue({
      amount: this.currencyMask.transform(form.amount)
    }, {
      emitEvent: false
    });
  }
});

Cela nécessite également que votre pipe "déformate" ce qui était là, ce qui est généralement aussi simple que quelque chose comme ceci dans la fonction de transformation de votre pipe :

value = value.replace(/\$+/g, '');

0 votes

Ça ne marche pas. Si vous modifiez la valeur en cliquant sur la touche Backspace. Par exemple, la valeur originale était 3, vous voulez la changer, cliquez sur la touche Backspace, elle devient 30. Cliquez à nouveau sur la touche d'effacement arrière, la valeur devient 300 et ainsi de suite.

0voto

joh04667 Points 3332

Sans connaître le code de votre pipe, il est probable que des erreurs se produisent à cause de l'endroit où vous construisez ce formulaire.

Essayez d'utiliser les crochets de détection des changements d'Angular pour définir cette valeur après que les entrées ont été résolues :

export class MyComponent implements OnInit {
  myForm: FormGroup;

  constructor(private builder: FormBuilder) { }    

  ngOnInit() {
    this.myForm = builder.group({
      amount: ['', Validators.required]
    });
  }

}

0 votes

La construction de mon formulaire se faisait dans le constructeur, mais elle dépendait d'autres données récupérées à partir d'une requête http, donc même si elle n'était pas dans le crochet ngOnInit, je pense qu'elle était appelée après ngOnInit. Pour tester, j'ai déplacé la construction à l'intérieur de ngOnInit et je rencontre toujours l'erreur

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X