Jak obserwować zmiany formy w Angular


151

W Angular mógłbym mieć formularz, który wygląda następująco:

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

W ramach odpowiedniego kontrolera mogłem łatwo obserwować zmiany w treści tego formularza, takie jak:

function($scope) {

    $scope.model = {};

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

}

Oto przykład Angular w JSFiddle .

Mam problem ze zrozumieniem, jak osiągnąć to samo w Angular. Oczywiście nie mamy już $scope$ rootScope. Z pewnością istnieje metoda, dzięki której można osiągnąć to samo?

Oto przykład Angulara na Plunker .


Zamiast oglądać niektóre dane z kontrolera, myślę, że powinieneś uruchomić zdarzenie (takie jak stara zmiana ng) ze swojego formularza.
Deblaton Jean-Philippe

btw, powodem usunięcia lunety jest pozbycie się tych obserwatorów. Chyba nie ma zegarka ukrytego gdzieś w kątowej 2
Deblaton Jean-Philippe

4
Jeśli dobrze cię rozumiem, sugerujesz, żebym dodał (ngModelChange)="onModelChange($event)"atrybut do każdego pola wejściowego w formularzu, aby to osiągnąć?
tambler

1
Czy samo wystąpienie formularza nie emituje jakiegoś zdarzenia zmiany? Jeśli tak, jak uzyskać do niego dostęp?
tambler

Gdybym był pewien, co robić, odpowiedziałbym, zamiast komentować. Nie korzystałem jeszcze z angular 2.0, ale z tego, co przeczytałem, zegarek całkowicie zniknął, aby mieć szkielet oparty na zdarzeniach (zamiast głębokiego zegarka wykonywanego przy każdym
skrócie

Odpowiedzi:


189

UPD. Odpowiedź i wersja demonstracyjna są aktualizowane, aby były zgodne z najnowszą wersją Angular.


Możesz subskrybować całe zmiany formularza ze względu na fakt, że FormGroup reprezentujący formularz udostępnia valueChangeswłaściwość będącą instancją Observerable:

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

W takim przypadku musisz utworzyć formularz ręcznie przy użyciu FormBuilder . Coś takiego:

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

Sprawdź valueChangesw akcji w tym demo : http://plnkr.co/edit/xOz5xaQyMlRzSrgtt7Wn?p=preview


2
To jest bardzo blisko znaku. Aby potwierdzić - czy mówisz mi, że nie można zasubskrybować valueChangesemitera zdarzeń formularza, jeśli ten formularz jest zdefiniowany wyłącznie w szablonie? Innymi słowy - czy w konstruktorze składnika nie można uzyskać odniesienia do formularza, który został zdefiniowany wyłącznie w szablonie tego składnika, a nie za pomocą FormBuildera?
tambler

to jest właściwa odpowiedź. Jeśli nie masz konstruktora formularzy, tworzysz formularze oparte na szablonach. prawdopodobnie istnieje sposób, aby nadal uzyskać wstrzyknięcie formularza, ale jeśli chcesz Observable form.valueChanges, powinieneś definitywnie użyć formBuilder i porzucić ng-model
Angular University

2
@tambler, możesz uzyskać odniesienie do NgForm za pomocą @ViewChild(). Zobacz moją zaktualizowaną odpowiedź.
Mark Rajcok,

1
Nie musisz rezygnować z subskrypcji na zniszczyć?
Bazinga

1
@galvan Nie sądzę, że może być wyciek. Formularz jest częścią komponentu i zostanie odpowiednio usunięty po zniszczeniu wraz ze wszystkimi jego polami i odbiornikami zdarzeń.
dfsq,

107

Jeśli używasz FormBuilder, zobacz odpowiedź @ dfsq.

Jeśli nie używasz FormBuilder, istnieją dwa sposoby powiadamiania o zmianach.

Metoda 1

Jak omówiono w komentarzach do pytania, użyj powiązania zdarzenia dla każdego elementu wejściowego. Dodaj do swojego szablonu:

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

Następnie w swoim komponencie:

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

Strona Formularze zawiera dodatkowe informacje o ngModel, które są istotne tutaj:

Nie ngModelChangejest <input>zdarzeniem elementu. W rzeczywistości jest to właściwość zdarzenia NgModeldyrektywy. Gdy Angular widzi element docelowy powiązania w formularzu [(x)], oczekuje, że xdyrektywa będzie miała xwłaściwość wejściową i xChangewłaściwość wyjściową.

Drugą osobliwością jest wyrazem szablonu model.name = $event. Jesteśmy przyzwyczajeni do widoku $eventobiektu pochodzącego ze zdarzenia DOM. Właściwość ngModelChange nie generuje zdarzenia DOM; jest to EventEmitterwłaściwość Angular, która zwraca wartość pola wejściowego po uruchomieniu.

Prawie zawsze wolimy [(ngModel)]. Moglibyśmy podzielić powiązanie, gdybyśmy musieli zrobić coś specjalnego w obsłudze zdarzenia, na przykład odbić lub zdławić naciśnięcia klawiszy.

W twoim przypadku przypuszczam, że chcesz zrobić coś specjalnego.

Metoda 2

Zdefiniuj zmienną szablonu lokalnego i ustaw ją na ngForm.
Użyj ngControl na elementach wejściowych.
Uzyskaj odwołanie do dyrektywy NgForm formularza za pomocą @ViewChild, a następnie zasubskrybuj ControlGroup NgForm w celu wprowadzenia zmian:

<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

Aby uzyskać więcej informacji na temat metody 2, zobacz wideo Savkina .

Zobacz także odpowiedź @ Thierry, aby uzyskać więcej informacji na temat tego, co możesz zrobić z valueChangesobserwowalnym (np. Odbicia / odczekania trochę przed przetworzeniem zmian).


61

Aby uzupełnić nieco więcej poprzednich świetnych odpowiedzi, musisz mieć świadomość, że formularze wykorzystują obserwowalne do wykrywania i obsługi zmian wartości. To coś naprawdę ważnego i potężnego. Zarówno Mark, jak i dfsq opisali ten aspekt w swoich odpowiedziach.

Observables pozwalają nie tylko na użycie subscribemetody (coś podobnego do thenmetody obietnic w Angular 1). W razie potrzeby możesz przejść dalej, aby zaimplementować niektóre łańcuchy przetwarzania dla zaktualizowanych danych w formularzach.

Chodzi mi o to, że na tym poziomie można określić czas odbicia debounceTimemetodą. Pozwala to odczekać pewien czas przed obsługą zmiany i poprawnie obsłużyć kilka danych wejściowych:

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

Możesz również bezpośrednio podłączyć przetwarzanie, które chcesz wyzwolić (na przykład asynchroniczne), gdy wartości są aktualizowane. Na przykład, jeśli chcesz obsłużyć wartość tekstową w celu przefiltrowania listy na podstawie żądania AJAX, możesz skorzystać z switchMapmetody:

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

Możesz nawet posunąć się dalej, łącząc zwrócone obserwowalne bezpośrednio z właściwością swojego komponentu:

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

i wyświetl go za pomocą asyncpotoku:

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

Powiem tylko, że musisz pomyśleć o sposobie obsługi formularzy w inny sposób w Angular2 (o wiele potężniejszy sposób ;-)).

Mam nadzieję, że ci to pomoże, Thierry


Właściwość „valueChanges” nie istnieje w typie „string”
Toolkit

Utknąłem w pliku TS, gdzie należy umieścić metodę zmiany formularza czeku? Jeśli przechowujemy go w ngAfterViewInit () {}, wydaje się, że this.form.valueChanges zawsze wywołuje, jeśli potrzebujemy zaimplementować jakieś łańcuchy przetwarzania dla zaktualizowanych danych w formularzach.
Tài Nguyễn

1

Rozwijając sugestie Marka ...

Metoda 3

Zaimplementuj w modelu wykrywanie „głębokich” zmian. Zalety polegają głównie na unikaniu włączania aspektów interfejsu użytkownika do komponentu; obejmuje to również zmiany programowe wprowadzone w modelu. To powiedziawszy, wymagałoby dodatkowej pracy, aby zaimplementować takie rzeczy, jak debouncing, jak zasugerował Thierry, a to również wyłapie twoje własne zmiany programistyczne, więc używaj go ostrożnie.

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

Spróbuj w Plunker


1

Do 5+wersji kątowej . Wprowadzenie wersji pomaga, ponieważ angular wprowadza wiele zmian.

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

0

Myślałem o zastosowaniu metody (ngModelChange), potem pomyślałem o metodzie FormBuilder i ostatecznie zdecydowałem się na odmianę metody 3. Oszczędza to dekorowanie szablonu dodatkowymi atrybutami i automatycznie przejmuje zmiany w modelu - zmniejszając możliwość zapomnienia o czymś z metodą 1 lub 2.

Upraszczając nieco metodę 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));
    }
}

Możesz dodać limit czasu, aby wywołać funkcję doSomething () tylko po x liczbie milisekund, aby zasymulować 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));
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.