Função de retorno de chamada de passagem angular para componente filho como @Input semelhante à maneira AngularJS

227

O AngularJS possui os parâmetros & nos quais você pode passar um retorno de chamada para uma diretiva (por exemplo, maneira de retorno de chamada AngularJS . É possível passar um retorno de chamada como um @Inputcomponente angular (algo como abaixo)? AngularJS faz?

@Component({
    selector: 'suggestion-menu',
    providers: [SuggestService],
    template: `
    <div (mousedown)="suggestionWasClicked(suggestion)">
    </div>`,
    changeDetection: ChangeDetectionStrategy.Default
})
export class SuggestionMenuComponent {
    @Input() callback: Function;

    suggestionWasClicked(clickedEntry: SomeModel): void {
        this.callback(clickedEntry, this.query);
    }
}


<suggestion-menu callback="insertSuggestion">
</suggestion-menu>
Michail Michailidis
fonte
6
para futuros leitores, o @Inputmodo sugerido fez com que meu código fosse complicado e não fosse fácil de manter. @Outputs são uma maneira muito mais natural de fazer o que eu quero. Como resultado eu mudei a resposta aceita
Michail Michailidis
A pergunta @IanS é sobre como algo é feito no Angular, semelhante ao AngularJS? por que o título é enganoso?
Michail Michailidis
Angular é muito diferente do AngularJS. Angular 2+ é apenas angular.
22619 Ian S
1
Corrigido seu título;)
Ian S
1
@IanS Thanks! agora a pergunta também é sobre angularJs - com a tag que você adicionou.
Michail Michailidis

Respostas:

296

Eu acho que é uma má solução. Se você deseja passar uma Função para o componente @Input(), @Output()decorador é o que você está procurando.

export class SuggestionMenuComponent {
    @Output() onSuggest: EventEmitter<any> = new EventEmitter();

    suggestionWasClicked(clickedEntry: SomeModel): void {
        this.onSuggest.emit([clickedEntry, this.query]);
    }
}

<suggestion-menu (onSuggest)="insertSuggestion($event[0],$event[1])">
</suggestion-menu>
Serginho
fonte
45
Para ser mais preciso, você não está passando a função, mas conectando um ouvinte de evento do ouvinte à saída. Útil para entender por que funciona.
Jens
13
Este é um ótimo método, mas fiquei com muitas perguntas depois de ler esta resposta. Eu esperava que fosse mais aprofundado ou que um link fosse fornecido descrevendo @Outpute EventEmitter. Então, aqui está a documentação angular do @Output para os interessados.
WebWanderer 19/01/19
9
Isso é bom para a ligação unidirecional. Você pode conectar-se ao evento da criança. Mas você não pode passar uma função de retorno de chamada para a criança e deixá-la analisar o valor de retorno da chamada de retorno. A resposta abaixo permite isso.
rook
3
Eu esperaria ter mais explicações sobre por que preferir uma maneira ou outra em vez de ter "Eu acho que é uma má solução".
Fidan Hakaj
6
Provavelmente bom para 80% dos casos, mas não quando um componente filho deseja uma visualização condicionada à existência de um retorno de chamada.
19418 John Freeman
115

ATUALIZAR

Esta resposta foi enviada quando o Angular 2 ainda estava em alfa e muitos dos recursos estavam indisponíveis / não documentados. Embora o abaixo ainda funcione, este método agora está totalmente desatualizado. Eu recomendo fortemente a resposta aceita acima.

Resposta original

Sim, na verdade é, no entanto, você deve ter certeza de que o escopo está correto. Para isso, usei uma propriedade para garantir que isso thissignifica o que eu quero.

@Component({
  ...
  template: '<child [myCallback]="theBoundCallback"></child>',
  directives: [ChildComponent]
})
export class ParentComponent{
  public theBoundCallback: Function;

  public ngOnInit(){
    this.theBoundCallback = this.theCallback.bind(this);
  }

  public theCallback(){
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}
SnareChops
fonte
1
Isso funcionou! Obrigado! Desejo a documentação tinha que em algum lugar :)
Michail Michailidis
1
Você pode usar um método estático, se desejar, mas não terá acesso a nenhum membro da instância do componente. Portanto, provavelmente não é seu caso de uso. Mas sim, você precisaria passar isso também deParent -> Child
SnareChops
3
Ótima resposta! Eu normalmente não renomeio a função ao vincular embora. no ngOnIniteu apenas usaria: this.theCallback = this.theCallback.bind(this)e então você pode passar adiante em theCallbackvez de theBoundCallback.
Zack
1
@MichailMichailidis Sim, concordo com sua solução e atualizei minha resposta com uma nota para levar as pessoas a um caminho melhor. Obrigado por ficar de olho neste.
SnareChops
7
@Output e EventEmitter são bons para uma ligação unidirecional. Você pode conectar-se ao evento da criança, mas não pode passar uma função de retorno de chamada para a criança e permitir que ela analise o valor de retorno da chamada de retorno. Essa resposta permite isso.
rook
31

Uma alternativa para a resposta que SnareChops deu.

Você pode usar .bind (this) no seu modelo para ter o mesmo efeito. Pode não ser tão limpo, mas salva algumas linhas. Atualmente, estou no angular 2.4.0

@Component({
  ...
  template: '<child [myCallback]="theCallback.bind(this)"></child>',
  directives: [ChildComponent]
})
export class ParentComponent {

  public theCallback(){
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}
Max Fahl
fonte
2
como outras pessoas comentaram bind (this) no modelo, não está documentado em nenhum lugar; portanto, ele poderá se tornar obsoleto / não suportado no futuro. Plus novamente @Inputestá causando o código para se tornar espaguete e usando @Outputos resultados em um processo mais natural / desembaraçados
Michail Michailidis
1
Quando você coloca bind () no modelo, o Angular reavalia essa expressão a cada detecção de alteração. A outra solução - fazer a ligação fora do modelo - é menos concisa, mas não tem esse problema.
31418 Chris
pergunta: ao fazer .bind (this), você está vinculando o método CallBack ao filho ou pai? Eu acho que é com a criança. Mas o problema é que, quando o vínculo está sendo chamado, é sempre a criança que está chamando, portanto esse vínculo não parece necessário se eu estiver correto.
ChrisZ
Ele se liga ao componente pai. O motivo disso é que, quando o CallBack () está sendo chamado, ele provavelmente fará algo dentro de si e, se "this" não for o componente pai, ficará fora de contexto e, portanto, não poderá alcançar seus próprios métodos e variáveis. não mais.
Max Fahl
29

Em alguns casos, pode ser necessário que a lógica comercial seja executada por um componente pai. No exemplo abaixo, temos um componente filho que renderiza a linha da tabela, dependendo da lógica fornecida pelo componente pai:

@Component({
  ...
  template: '<table-component [getRowColor]="getColor"></table-component>',
  directives: [TableComponent]
})
export class ParentComponent {

 // Pay attention on the way this function is declared. Using fat arrow (=>) declaration 
 // we can 'fixate' the context of `getColor` function
 // so that it is bound to ParentComponent as if .bind(this) was used.
 getColor = (row: Row) => {
    return this.fancyColorService.getUserFavoriteColor(row);
 }

}

@Component({...})
export class TableComponent{
  // This will be bound to the ParentComponent.getColor. 
  // I found this way of declaration a bit safer and convenient than just raw Function declaration
  @Input('getRowColor') getRowColor: (row: Row) => Color;

  renderRow(){
    ....
    // Notice that `getRowColor` function holds parent's context because of a fat arrow function used in the parent
    const color = this.getRowColor(row);
    renderRow(row, color);
  }
}

Então, eu queria demonstrar duas coisas aqui:

  1. A seta gorda (=>) funciona em vez de .bind (this) para manter o contexto correto;
  2. Declaração segura de uma função de retorno de chamada no componente filho.
Danylo Zatorsky
fonte
1
Grande explicação para o uso da seta gordura para substituir o uso de.bind(this)
TYMG
6
Dica de uso: Certifique-se de colocar [getRowColor]="getColor"e não [getRowColor]="getColor()";-)
Simon_Weaver
Agradável. Era exatamente isso que eu estava procurando. Simples e eficaz.
BrainSlugs83 12/03
7

Como exemplo, estou usando uma janela modal de login, em que a janela modal é o pai, o formulário de login é o filho e o botão de login chama de volta para a função de fechamento do pai modal.

O modal pai contém a função para fechar o modal. Esse pai passa a função close para o componente filho de logon.

import { Component} from '@angular/core';
import { LoginFormComponent } from './login-form.component'

@Component({
  selector: 'my-modal',
  template: `<modal #modal>
      <login-form (onClose)="onClose($event)" ></login-form>
    </modal>`
})
export class ParentModalComponent {
  modal: {...};

  onClose() {
    this.modal.close();
  }
}

Depois que o componente de login filho envia o formulário de login, ele fecha o modal pai usando a função de retorno de chamada do pai

import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'login-form',
  template: `<form (ngSubmit)="onSubmit()" #loginForm="ngForm">
      <button type="submit">Submit</button>
    </form>`
})
export class ChildLoginComponent {
  @Output() onClose = new EventEmitter();
  submitted = false;

  onSubmit() {
    this.onClose.emit();
    this.submitted = true;
  }
}
Camilla Kydland
fonte
7

Uma alternativa à resposta que Max Fahl deu.

Você pode definir a função de retorno de chamada como uma função de seta no componente pai para não precisar vinculá-lo.

@Component({
  ...
  // unlike this, template: '<child [myCallback]="theCallback.bind(this)"></child>',
  template: '<child [myCallback]="theCallback"></child>',
  directives: [ChildComponent]
})
export class ParentComponent {

   // unlike this, public theCallback(){
   public theCallback = () => {
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}

jeadonara
fonte
5

Passando método com argumento, usando .bind dentro do modelo

@Component({
  ...
  template: '<child [action]="foo.bind(this, 'someArgument')"></child>',
  ...
})
export class ParentComponent {
  public foo(someParameter: string){
    ...
  }
}

@Component({...})
export class ChildComponent{

  @Input()
  public action: Function; 

  ...
}
Shogg
fonte
Sua resposta não é essencialmente a mesma: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis
respondendo a este comentário stackoverflow.com/questions/35328652/…
Shogg 15/06/19
0

Use padrão observável. Você pode colocar o valor observável (não o assunto) no parâmetro Input e gerenciá-lo a partir do componente pai. Você não precisa da função de retorno de chamada.

Veja o exemplo: https://stackoverflow.com/a/49662611/4604351

Alexey Baranoshnikov
fonte
você pode ilustrá-lo com um exemplo de trabalho?
Michail Michailidis
0

Outra alternativa.

O OP pediu uma maneira de usar um retorno de chamada. Nesse caso, ele estava se referindo especificamente a uma função que processa um evento (em seu exemplo: um evento de clique), que deve ser tratado como a resposta aceita por @serginho sugere: with @Outpute EventEmitter.

No entanto, há uma diferença entre um retorno de chamada e um evento: Com um retorno de chamada, o componente filho pode recuperar alguns comentários ou informações dos pais, mas um evento pode apenas informar que algo aconteceu sem esperar nenhum retorno.

Existem casos de uso em que um feedback é necessário, por exemplo. obtenha uma cor ou uma lista de elementos que o componente precisa manipular. Você pode usar funções vinculadas, como algumas respostas sugeriram, ou pode usar interfaces (essa é sempre a minha preferência).

Exemplo

Vamos supor que você tenha um componente genérico que opera sobre uma lista de elementos {id, nome} que você deseja usar com todas as suas tabelas de banco de dados que possuem esses campos. Este componente deve:

  • recuperar um intervalo de elementos (página) e mostrá-los em uma lista
  • permitir remover um elemento
  • informe que um elemento foi clicado, para que o pai possa executar algumas ações.
  • permitir recuperar a próxima página de elementos.

Componente filho

Usando a ligação normal, precisaríamos de 1 @Input()e 3 @Output()parâmetros (mas sem nenhum feedback do pai). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, mas, ao criar uma interface, precisaremos de apenas uma @Input():

import {Component, Input, OnInit} from '@angular/core';

export interface IdName{
  id: number;
  name: string;
}

export interface IListComponentCallback<T extends IdName> {
    getList(page: number, limit: number): Promise< T[] >;
    removeItem(item: T): Promise<boolean>;
    click(item: T): void;
}

@Component({
    selector: 'list-ctrl',
    template: `
      <button class="item" (click)="loadMore()">Load page {{page+1}}</button>
      <div class="item" *ngFor="let item of list">
          <button (click)="onDel(item)">DEL</button>
          <div (click)="onClick(item)">
            Id: {{item.id}}, Name: "{{item.name}}"
          </div>
      </div>
    `,
    styles: [`
      .item{ margin: -1px .25rem 0; border: 1px solid #888; padding: .5rem; width: 100%; cursor:pointer; }
      .item > button{ float: right; }
      button.item{margin:.25rem;}
    `]
})
export class ListComponent implements OnInit {
    @Input() callback: IListComponentCallback<IdName>; // <-- CALLBACK
    list: IdName[];
    page = -1; 
    limit = 10;

    async ngOnInit() {
      this.loadMore();
    }
    onClick(item: IdName) {
      this.callback.click(item);   
    }
    async onDel(item: IdName){ 
        if(await this.callback.removeItem(item)) {
          const i = this.list.findIndex(i=>i.id == item.id);
          this.list.splice(i, 1);
        }
    }
    async loadMore(){
      this.page++;
      this.list = await this.callback.getList(this.page, this.limit); 
    }
}

Componente pai

Agora podemos usar o componente list no pai.

import { Component } from "@angular/core";
import { SuggestionService } from "./suggestion.service";
import { IdName, IListComponentCallback } from "./list.component";

type Suggestion = IdName;

@Component({
  selector: "my-app",
  template: `
    <list-ctrl class="left" [callback]="this"></list-ctrl>
    <div class="right" *ngIf="msg">{{ msg }}<br/><pre>{{item|json}}</pre></div>
  `,
  styles:[`
    .left{ width: 50%; }
    .left,.right{ color: blue; display: inline-block; vertical-align: top}
    .right{max-width:50%;overflow-x:scroll;padding-left:1rem}
  `]
})
export class ParentComponent implements IListComponentCallback<Suggestion> {
  msg: string;
  item: Suggestion;

  constructor(private suggApi: SuggestionService) {}

  getList(page: number, limit: number): Promise<Suggestion[]> {
    return this.suggApi.getSuggestions(page, limit);
  }
  removeItem(item: Suggestion): Promise<boolean> {
    return this.suggApi.removeSuggestion(item.id)
      .then(() => {
        this.showMessage('removed', item);
        return true;
      })
      .catch(() => false);
  }
  click(item: Suggestion): void {
    this.showMessage('clicked', item);
  }
  private showMessage(msg: string, item: Suggestion) {
    this.item = item;
    this.msg = 'last ' + msg;
  }
}

Observe que o <list-ctrl>recebimento this(componente pai) é o objeto de retorno de chamada. Uma vantagem adicional é que não é necessário enviar a instância pai, ela pode ser um serviço ou qualquer objeto que implemente a interface, se o seu caso de uso permitir.

O exemplo completo está neste stackblitz .

WPomier
fonte
-3

A resposta atual pode ser simplificada para ...

@Component({
  ...
  template: '<child [myCallback]="theCallback"></child>',
  directives: [ChildComponent]
})
export class ParentComponent{
  public theCallback(){
    ...
  }
}

@Component({...})
export class ChildComponent{
  //This will be bound to the ParentComponent.theCallback
  @Input()
  public myCallback: Function; 
  ...
}
Azul
fonte
então não há necessidade de vincular explicitamente?
Michail Michailidis 23/03
3
Sem .bind(this)isso, o thisinterior do retorno de chamada será o windowque pode não interessar, dependendo do seu caso de uso. No entanto, se você tiver thiso retorno de chamada, .bind(this)é necessário. Caso contrário, esta versão simplificada é o caminho a seguir.
SnareChops 23/03
3
Eu recomendo sempre vincular o retorno de chamada ao componente, porque eventualmente você usará thisdentro da função de retorno de chamada. É apenas propenso a erros.
Alexandre Junges
Esse é um exemplo de um antipadrão Angular 2.
Serginho
Não precisa ser um antipadrão. Há casos em que você deseja exatamente isso. Não é incomum querer dizer ao componente como fazer algo que não é sobre a exibição. Faz sentido e não vejo por que essa resposta está recebendo tanto ódio.
Lazar Ljubenović