Comment surveiller les changements de forme dans Angular

151

Dans Angular, je pourrais avoir une forme qui ressemble à ceci:

<ng-form>
    <label>First Name</label>
    <input type="text" ng-model="model.first_name">

    <label>Last Name</label>
    <input type="text" ng-model="model.last_name">
</ng-form>

Dans le contrôleur correspondant, je pourrais facilement surveiller les modifications du contenu de ce formulaire comme ceci:

function($scope) {

    $scope.model = {};

    $scope.$watch('model', () => {
        // Model has updated
    }, true);

}

Voici un exemple angulaire sur JSFiddle .

J'ai du mal à comprendre comment accomplir la même chose dans Angular. De toute évidence, nous n'avons plus $scope, $ rootScope. Il existe certainement une méthode par laquelle la même chose peut être accomplie?

Voici un exemple angulaire sur Plunker .

dompteur
la source
au lieu de regarder certaines données de votre contrôleur, je pense que vous devriez déclencher un événement (comme l'ancien ng-change) depuis votre formulaire.
Deblaton Jean-Philippe
btw, la raison de la suppression de la portée est de se débarrasser de ces observateurs. Je ne pense pas qu'il y ait une montre cachée quelque part dans angulaire 2
Deblaton Jean-Philippe
4
Si je vous comprends bien, vous suggérez que j'ajoute un (ngModelChange)="onModelChange($event)"attribut à chaque entrée de formulaire afin d'accomplir cela?
tambler
1
L'instance de formulaire elle-même n'émet-elle pas une sorte d'événement de changement? Si oui, comment y accédez-vous?
tambler
Si j'étais sûr de ce qu'il faut faire, j'aurais répondu au lieu de commenter. Je n'ai pas encore utilisé angular 2.0, mais d'après ce que j'ai lu, la montre a complètement disparu pour avoir un cadre basé sur les événements (au lieu de la montre profonde effectuée à chaque résumé)
Deblaton Jean-Philippe

Réponses:

189

UPD. La réponse et la démo sont mises à jour pour s'aligner sur le dernier Angular.


Vous pouvez vous abonner à des modifications de formulaire entières car FormGroup représentant un formulaire fournit une valueChangespropriété qui est une instance observable :

this.form.valueChanges.subscribe(data => console.log('Form changes', data));

Dans ce cas, vous devrez créer un formulaire manuellement à l'aide de FormBuilder . Quelque chose comme ça:

export class App {
  constructor(private formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      firstName: 'Thomas',
      lastName: 'Mann'
    })

    this.form.valueChanges.subscribe(data => {
      console.log('Form changes', data)
      this.output = data
    })
  }
}

Découvrez valueChangesen action dans cette démo : http://plnkr.co/edit/xOz5xaQyMlRzSrgtt7Wn?p=preview

dfsq
la source
2
C'est très proche de la marque. Pour confirmer - me dites-vous qu'il n'est pas possible de souscrire à l' valueChangesémetteur d'événements d' un formulaire si ce formulaire est défini uniquement dans le modèle? En d'autres termes - dans le constructeur d'un composant, n'est-il pas possible d'obtenir une référence à un formulaire qui a été défini uniquement dans le modèle de ce composant et non avec l'aide de FormBuilder?
tambler
c'est la bonne réponse. Si vous n'avez pas de générateur de formulaire, vous créez des formulaires basés sur des modèles. il existe probablement un moyen de toujours faire injecter le formulaire, mais si vous voulez le formulaire observable.valueChanges, vous devez définitivement utiliser formBuilder et abandonner ng-model
Angular University
2
@tambler, vous pouvez obtenir une référence à NgForm en utilisant @ViewChild(). Voir ma réponse mise à jour.
Mark Rajcok
1
Vous n'avez pas besoin de vous désinscrire lors de la destruction?
Bazinga
1
@galvan Je ne pense pas qu'il puisse y avoir une fuite. Le formulaire fait partie du composant et il sera correctement éliminé lors de la destruction avec tous ses champs et écouteurs d'événements.
dfsq
107

Si vous utilisez FormBuilder, consultez la réponse de @ dfsq.

Si vous n'utilisez pas FormBuilder, il existe deux façons d'être notifié des modifications.

Méthode 1

Comme indiqué dans les commentaires sur la question, utilisez une liaison d'événement sur chaque élément d'entrée. Ajoutez à votre modèle:

<input type="text" class="form-control" required [ngModel]="model.first_name"
         (ngModelChange)="doSomething($event)">

Puis dans votre composant:

doSomething(newValue) {
  model.first_name = newValue;
  console.log(newValue)
}

La page Formulaires contient des informations supplémentaires sur ngModel qui sont pertinentes ici:

L' événement ngModelChangen'est pas un <input>élément. Il s'agit en fait d'une propriété événementielle de la NgModeldirective. Lorsque Angular voit une cible de liaison dans le formulaire [(x)], il s'attend à ce que la xdirective ait une xpropriété d'entrée et une xChangepropriété de sortie.

L'autre bizarrerie est l'expression du modèle, model.name = $event. Nous avons l'habitude de voir un $eventobjet provenant d'un événement DOM. La propriété ngModelChange ne produit pas d'événement DOM; c'est une EventEmitterpropriété Angular qui renvoie la valeur de la zone de saisie lorsqu'elle se déclenche.

Nous préférons presque toujours [(ngModel)]. Nous pourrions diviser la liaison si nous devions faire quelque chose de spécial dans la gestion des événements, comme l'anti-rebond ou la limitation des touches.

Dans votre cas, je suppose que vous voulez faire quelque chose de spécial.

Méthode 2

Définissez une variable de modèle locale et définissez-la sur ngForm.
Utilisez ngControl sur les éléments d'entrée.
Obtenez une référence à la directive NgForm du formulaire à l'aide de @ViewChild, puis abonnez-vous au ControlGroup de NgForm pour les modifications:

<form #myForm="ngForm" (ngSubmit)="onSubmit()">
  ....
  <input type="text" ngControl="firstName" class="form-control" 
   required [(ngModel)]="model.first_name">
  ...
  <input type="text" ngControl="lastName" class="form-control" 
   required [(ngModel)]="model.last_name">

class MyForm {
  @ViewChild('myForm') form;
  ...
  ngAfterViewInit() {
    console.log(this.form)
    this.form.control.valueChanges
      .subscribe(values => this.doSomething(values));
  }
  doSomething(values) {
    console.log(values);
  }
}

plunker

Pour plus d'informations sur la méthode 2, voir la vidéo de Savkin .

Voir aussi la réponse de @ Thierry pour plus d'informations sur ce que vous pouvez faire avec l' valueChangesobservable (comme le débouncing / attendre un peu avant de traiter les changements).

Mark Rajcok
la source
61

Pour compléter un peu plus de bonnes réponses précédentes, vous devez être conscient que les formulaires utilisent des observables pour détecter et gérer les changements de valeur. C'est quelque chose de vraiment important et puissant. Mark et dfsq ont tous deux décrit cet aspect dans leurs réponses.

Les observables permettent non seulement d'utiliser la subscribeméthode (quelque chose de similaire à la thenméthode des promesses dans Angular 1). Vous pouvez aller plus loin si nécessaire pour implémenter certaines chaînes de traitement des données mises à jour dans les formulaires.

Je veux dire que vous pouvez spécifier à ce niveau le temps anti-rebond avec la debounceTimeméthode. Cela vous permet d'attendre un certain temps avant de traiter le changement et de gérer correctement plusieurs entrées:

this.form.valueChanges
    .debounceTime(500)
    .subscribe(data => console.log('form changes', data));

Vous pouvez également brancher directement le traitement que vous souhaitez déclencher (certains asynchrones par exemple) lorsque les valeurs sont mises à jour. Par exemple, si vous souhaitez gérer une valeur de texte pour filtrer une liste en fonction d'une requête AJAX, vous pouvez tirer parti de la switchMapméthode:

this.textValue.valueChanges
    .debounceTime(500)
    .switchMap(data => this.httpService.getListValues(data))
    .subscribe(data => console.log('new list values', data));

Vous allez même plus loin en liant l'observable retournée directement à une propriété de votre composant:

this.list = this.textValue.valueChanges
    .debounceTime(500)
    .switchMap(data => this.httpService.getListValues(data))
    .subscribe(data => console.log('new list values', data));

et affichez-le à l'aide du asynctube:

<ul>
  <li *ngFor="#elt of (list | async)">{{elt.name}}</li>
</ul>

Juste pour dire que vous devez penser à la manière de gérer les formulaires différemment dans Angular2 (une manière beaucoup plus puissante ;-)).

J'espère que ça t'aide, Thierry

Thierry Templier
la source
La propriété 'valueChanges' n'existe pas sur le type 'string'
Toolkit
J'ai coincé, dans le fichier TS, où la méthode de changement de formulaire de chèque doit être placée? Si nous le stockons dans ngAfterViewInit () {}, il semble que les this.form.valueChanges appellent toujours si nous devions implémenter des chaînes de traitement pour les données mises à jour dans les formulaires.
Tài Nguyễn
1

Élargissement des suggestions de Mark ...

Méthode 3

Implémentez la détection des changements "profonds" sur le modèle. Les avantages consistent principalement à éviter d'incorporer des aspects d'interface utilisateur dans le composant; cela prend également en compte les modifications programmatiques apportées au modèle. Cela dit, cela nécessiterait un travail supplémentaire pour implémenter des choses telles que le debouncing comme suggéré par Thierry, et cela capturera également vos propres changements programmatiques, donc utilisez avec prudence.

export class App implements DoCheck {
  person = { first: "Sally", last: "Jones" };
  oldPerson = { ...this.person }; // ES6 shallow clone. Use lodash or something for deep cloning

  ngDoCheck() {
    // Simple shallow property comparison - use fancy recursive deep comparison for more complex needs
    for (let prop in this.person) {
      if (this.oldPerson[prop] !==  this.person[prop]) {
        console.log(`person.${prop} changed: ${this.person[prop]}`);
        this.oldPerson[prop] = this.person[prop];
      }
    }
  }

Essayez dans Plunker

N8allan
la source
1

Pour la 5+version angulaire . Mettre la version aide car angulaire fait beaucoup de changements.

ngOnInit() {

 this.myForm = formBuilder.group({
      firstName: 'Thomas',
      lastName: 'Mann'
    })
this.formControlValueChanged() // Note if you are doing an edit/fetching data from an observer this must be called only after your form is properly initialized otherwise you will get error.
}

formControlValueChanged(): void {       
        this.myForm.valueChanges.subscribe(value => {
            console.log('value changed', value)
        })
}
PC Krishnadas
la source
0

J'ai pensé à utiliser la méthode (ngModelChange), puis j'ai pensé à la méthode FormBuilder, et j'ai finalement opté pour une variante de la méthode 3. Cela évite de décorer le modèle avec des attributs supplémentaires et prend automatiquement les modifications du modèle - réduisant la possibilité d'oublier quelque chose avec la méthode 1 ou 2.

Simplifier un peu la méthode 3 ...

oldPerson = JSON.parse(JSON.stringify(this.person));

ngDoCheck(): void {
    if (JSON.stringify(this.person) !== JSON.stringify(this.oldPerson)) {
        this.doSomething();
        this.oldPerson = JSON.parse(JSON.stringify(this.person));
    }
}

Vous pouvez ajouter un délai pour n'appeler doSomething () qu'après x nombre de millisecondes pour simuler le debounce.

oldPerson = JSON.parse(JSON.stringify(this.person));

ngDoCheck(): void {
    if (JSON.stringify(this.person) !== JSON.stringify(this.oldPerson)) {
        if (timeOut) clearTimeout(timeOut);
        let timeOut = setTimeout(this.doSomething(), 2000);
        this.oldPerson = JSON.parse(JSON.stringify(this.person));
    }
}
Ulfius
la source