Como observar as alterações de formulário no Angular

151

Em Angular, eu posso ter um formulário que se parece com isso:

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

Dentro do controlador correspondente, eu pude observar facilmente as alterações no conteúdo desse formulário da seguinte forma:

function($scope) {

    $scope.model = {};

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

}

Aqui está um exemplo angular no JSFiddle .

Estou tendo problemas para descobrir como realizar a mesma coisa no Angular. Obviamente, não temos mais $scope$ rootScope. Certamente existe um método pelo qual a mesma coisa pode ser realizada?

Aqui está um exemplo angular no Plunker .

tambler
fonte
em vez de assistir a alguns dados do seu controlador, acho que você deve disparar um evento (como a antiga ng-change) do seu formulário.
Deblaton Jean-Philippe 5/16/16
Aliás, a razão para remover o escopo é se livrar desses observadores. Eu não acho que há um relógio em algum lugar escondido em angular 2
Deblaton Jean-Philippe
4
Se estou entendendo corretamente, você está sugerindo que eu adicione um (ngModelChange)="onModelChange($event)"atributo a cada entrada de formulário para fazer isso?
tambler
1
A instância do formulário em si não emite algum tipo de evento de alteração? Se sim, como você acessa?
tambler
Se eu tivesse certeza do que fazer, teria respondido em vez de comentar. Ainda não usei o angular 2.0, mas pelo que li, o relógio desapareceu completamente para ter uma estrutura baseada em eventos (em vez da observação profunda realizada a cada resumo)
Deblaton Jean-Philippe

Respostas:

189

UPD. A resposta e a demonstração são atualizadas para alinhar com o Angular mais recente.


Você pode se inscrever em alterações inteiras do formulário devido ao fato de o FormGroup representar um formulário fornecer valueChangespropriedade que é uma instância Observerable:

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

Nesse caso, você precisaria construir o formulário manualmente usando o FormBuilder . Algo assim:

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

Confira valueChangesem ação nesta demonstração : http://plnkr.co/edit/xOz5xaQyMlRzSrgtt7Wn?p=preview

dfsq
fonte
2
Isso está muito próximo da marca. Para confirmar - você está me dizendo que é não possível assinar um formulário valueChangesemissor de evento , se essa forma é definida apenas dentro do modelo? Em outras palavras - no construtor de um componente, não é possível obter uma referência a um formulário que foi definido apenas dentro do modelo desse componente e não com a ajuda do FormBuilder?
tambler
Esta é a resposta certa. Se você não possui um construtor de formulários, está executando formulários orientados a modelos. provavelmente há uma maneira de ainda obter o formulário injetado mas se você quiser os form.valueChanges observáveis você deve definitivamente usar formbuilder e abandonar NG-modelo
Universidade Angular
2
@ tambler, você pode obter uma referência ao NgForm usando @ViewChild(). Veja minha resposta atualizada.
Mark Rajcok
1
Você não precisa cancelar a inscrição em destruir?
Bazinga
1
Galvan @ Eu não acho que pode haver um vazamento. O formulário faz parte do componente e será descartado corretamente na destruição com todos os seus campos e ouvintes de eventos.
dfsq
107

Se você estiver usando FormBuilder, consulte a resposta de @ dfsq.

Se você não estiver usando FormBuilder, há duas maneiras de ser notificado sobre alterações.

Método 1

Conforme discutido nos comentários sobre a pergunta, use uma ligação de evento em cada elemento de entrada. Adicione ao seu modelo:

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

Então no seu componente:

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

A página Formulários possui algumas informações adicionais sobre o ngModel que são relevantes aqui:

o ngModelChange<input> evento não é um elemento. Na verdade, é uma propriedade de evento da NgModeldiretiva. Quando o Angular vê um destino de ligação no formulário [(x)], espera que a xdiretiva tenha uma xpropriedade de entrada e uma xChangepropriedade de saída.

A outra singularidade é a expressão do modelo model.name = $event,. Estamos acostumados a ver um $eventobjeto proveniente de um evento DOM. A propriedade ngModelChange não produz um evento DOM; é uma EventEmitterpropriedade Angular que retorna o valor da caixa de entrada quando é acionada.

Quase sempre preferimos [(ngModel)]. Poderíamos dividir a ligação se tivéssemos que fazer algo especial no tratamento de eventos, como rejeitar ou acelerar os pressionamentos de tecla.

No seu caso, suponho que você queira fazer algo especial.

Método 2

Defina uma variável de modelo local e defina-a como ngForm.
Use ngControl nos elementos de entrada.
Obtenha uma referência à diretiva NgForm do formulário usando @ViewChild e assine o ControlGroup do NgForm para obter alterações:

<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

Para mais informações sobre o Método 2, consulte o vídeo de Savkin .

Consulte também a resposta de @ Thierry para obter mais informações sobre o que você pode fazer com o valueChangesobservável (como rebater / esperar um pouco antes de processar as alterações).

Mark Rajcok
fonte
61

Para concluir um pouco mais das excelentes respostas anteriores, você precisa estar ciente de que os formulários aproveitam os observáveis ​​para detectar e manipular alterações de valor. É algo realmente importante e poderoso. Mark e dfsq descreveram esse aspecto em suas respostas.

Observáveis ​​permitem não apenas usar o subscribemétodo (algo semelhante aothen método de promessas no Angular 1). Você pode ir além, se necessário, para implementar algumas cadeias de processamento para dados atualizados em formulários.

Quero dizer, você pode especificar nesse nível o tempo de debounce com o debounceTime método Isso permite que você aguarde um tempo antes de manipular a alteração e manipular corretamente várias entradas:

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

Você também pode conectar diretamente o processamento que deseja acionar (por exemplo, um assíncrono) quando os valores forem atualizados. Por exemplo, se você quiser manipular um valor de texto para filtrar uma lista com base em uma solicitação AJAX, poderá aproveitar oswitchMap método:

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

Você vai além, vinculando o observável retornado diretamente a uma propriedade do seu componente:

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

e exiba usando o asyncpipe:

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

Apenas para dizer que você precisa pensar na maneira de lidar com formas de maneira diferente no Angular2 (uma maneira muito mais poderosa ;-)).

Espero que ajude você, Thierry

Thierry Templier
fonte
A propriedade 'valueChanges' não existe no tipo 'string'
Toolkit
Fiquei preso no arquivo TS, onde o método de alteração do formulário de verificação deve ser colocado? Se o armazenarmos em ngAfterViewInit () {}, parece que o this.form.valueChanges sempre chama se for necessário implementar algumas cadeias de processamento para dados atualizados nos formulários.
Tài Nguyễn
1

Expandindo as sugestões de Mark ...

Método 3

Implemente a detecção de alterações "profunda" no modelo. As vantagens envolvem principalmente evitar a incorporação de aspectos da interface do usuário no componente; isso também captura as alterações programáticas feitas no modelo. Dito isso, seria necessário um trabalho extra para implementar coisas como a devolução, como sugerido por Thierry, e isso também capturará suas próprias alterações programáticas, portanto, use com cuidado.

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

Tente em Plunker

N8allan
fonte
1

Para 5+versão angular . Colocar a versão ajuda como angular faz muitas alterações.

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)
        })
}
Krishnadas PC
fonte
0

Pensei em usar o método (ngModelChange), depois pensei no método FormBuilder e, finalmente, decidi por uma variação do método 3. Isso economiza a decoração do modelo com atributos extras e captura automaticamente as alterações no modelo - reduzindo a possibilidade de esquecer algo com o método 1 ou 2.

Simplificando o método 3 um pouco ...

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

Você pode adicionar um tempo limite para chamar apenas doSomething () após um número x de milissegundos para simular a rejeição.

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
fonte