Función de devolución de llamada de paso angular al componente secundario como @Input similar a la forma AngularJS

227

AngularJS tiene los parámetros & donde puede pasar una devolución de llamada a una directiva (por ejemplo, la forma de devolución de llamada de AngularJS . ¿Es posible pasar una devolución de llamada como un @Inputcomponente angular (algo como a continuación)? Si no, ¿qué sería lo más parecido a qué? AngularJS hace?

@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
fuente
66
para los futuros lectores, la @Inputforma sugerida hizo que mi código sea un spagetti y no es fácil de mantener. @OutputEs una forma mucho más natural de hacer lo que quiero. Como resultado, cambié la respuesta aceptada
Michail Michailidis, el
La pregunta de @IanS es acerca de cómo se hace algo en Angular similar a AngularJS. ¿Por qué el título es engañoso?
Michail Michailidis
Angular es muy diferente de AngularJS. Angular 2+ es simplemente angular.
Ian S
1
Fixed your title;)
Ian S
1
@ Ian ¡Gracias! ahora la pregunta también es sobre angularJs, aunque con la etiqueta que agregó.
Michail Michailidis

Respuestas:

296

Creo que es una mala solución. Si desea pasar una Función a un componente con @Input(), @Output()decorador es lo que está buscando.

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
fuente
45
Para ser precisos, no está pasando la función, sino que conecta un oyente de eventos de escucha a la salida. Útil para entender por qué funciona.
Jens
13
Este es un gran método, pero me quedé con muchas preguntas después de leer esta respuesta. Esperaba que fuera más profundo o que se proporcionara un enlace que describiera @Outputy EventEmitter. Entonces, aquí está la documentación angular para @Output para aquellos interesados.
WebWanderer
99
Esto está bien para el enlace unidireccional. Puede conectarse al evento del niño. Pero no puede pasar una función de devolución de llamada al hijo y dejar que analice el valor de retorno de la devolución de llamada. La respuesta a continuación lo permite.
torre
3
Esperaría tener más explicaciones sobre por qué preferir una forma frente a otra en lugar de tener "Creo que es una mala solución".
Fidan Hakaj
66
Probablemente sea bueno para el 80% de los casos, pero no cuando un componente secundario quiere una visualización condicional a si existe una devolución de llamada.
John Freeman
115

ACTUALIZAR

Esta respuesta se envió cuando Angular 2 todavía estaba en alfa y muchas de las características no estaban disponibles / indocumentadas. Si bien lo siguiente seguirá funcionando, este método ahora está completamente desactualizado. Recomiendo encarecidamente la respuesta aceptada a continuación.

Respuesta original

Sí, de hecho lo es, sin embargo, querrás asegurarte de que tenga un alcance correcto. Para esto, he usado una propiedad para asegurar que eso thissignifica lo que quiero.

@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
fuente
1
Esto funcionó! ¡Gracias! Desearía que la documentación tuviera eso en alguna parte :)
Michail Michailidis
1
Si lo desea, puede usar un método estático, pero no tendrá acceso a ninguno de los miembros de la instancia del componente. Entonces, probablemente no sea su caso de uso. Pero sí, lo que tendría que pasar, así que a partirParent -> Child
SnareChops
3
¡Gran respuesta! Sin embargo, normalmente no cambio el nombre de la función al vincular. en ngOnInitme acaba de utilizar: this.theCallback = this.theCallback.bind(this)y entonces puede pasar de largo theCallbacken lugar de theBoundCallback.
Zack
1
@MichailMichailidis Sí, estoy de acuerdo con su solución y he actualizado mi respuesta con una nota para guiar a las personas hacia la mejor manera. Gracias por vigilar este.
SnareChops
77
@Output y EventEmitter están bien para un enlace unidireccional. Puede conectarse al evento del niño, pero no puede pasar una función de devolución de llamada al niño y dejar que analice el valor de retorno de la devolución de llamada. Esta respuesta lo permite.
torre
31

Una alternativa a la respuesta que SnareChops dio.

Puede usar .bind (this) en su plantilla para tener el mismo efecto. Puede que no sea tan limpio pero ahorra un par de líneas. Actualmente estoy en 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
fuente
2
como otros han comentado, el enlace (esto) en la plantilla no está documentado en ninguna parte, por lo que podría quedar obsoleto / no admitido en el futuro. Plus nuevamente @Inputestá causando que el código se convierta en espagueti y el uso de @Outputresultados en un proceso más natural / desenredado
Michail Michailidis
1
Cuando coloca bind () en la plantilla, Angular reevalúa esta expresión en cada detección de cambio. La otra solución, hacer el enlace fuera de la plantilla, es menos conciso, pero no tiene este problema.
Chris
pregunta: al hacer .bind (esto), ¿está vinculando el método theCallBack con el hijo o el padre? Creo que es con el niño. Pero la cuestión es que, cuando se llama al enlace, siempre lo llama el niño, por lo que este enlace no parece necesario si estoy en lo cierto.
ChrisZ
Se une con el componente padre. La razón por la que se hace esto es que cuando se llama a CallBack (), probablemente querrá hacer algo dentro de sí mismo, y si "esto" no es el componente padre, estará fuera de contexto y, por lo tanto, no podrá alcanzar sus propios métodos y variables nunca más.
Max Fahl
29

En algunos casos, es posible que necesite que un componente principal realice la lógica de negocios. En el siguiente ejemplo, tenemos un componente secundario que representa la fila de la tabla dependiendo de la lógica proporcionada por el componente principal:

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

Entonces, quería demostrar 2 cosas aquí:

  1. Fat arrow (=>) funciona en lugar de .bind (this) para mantener el contexto correcto;
  2. Declaración typesafe de una función de devolución de llamada en el componente secundario.
Danylo Zatorsky
fuente
1
Gran explicación para el uso de la flecha gorda para reemplazar el uso de.bind(this)
TYMG
66
[getRowColor]="getColor"[getRowColor]="getColor()"
Consejo de
Agradable. Esto es exactamente lo que estaba buscando. Simple y efectivo.
BrainSlugs83
7

Como ejemplo, estoy usando una ventana modal de inicio de sesión, donde la ventana modal es la principal, el formulario de inicio de sesión es el secundario y el botón de inicio de sesión llama a la función de cierre de la principal modal.

El modal padre contiene la función para cerrar el modal. Este padre pasa la función de cierre al componente hijo de inicio de sesión.

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

Después de que el componente de inicio de sesión secundario envíe el formulario de inicio de sesión, cierra el modal principal mediante la función de devolución de llamada principal

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

Una alternativa a la respuesta que dio Max Fahl.

Puede definir la función de devolución de llamada como una función de flecha en el componente principal para que no necesite vincular eso.

@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
fuente
5

Pasar método con argumento, usando .bind dentro de la plantilla

@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
fuente
¿No es su respuesta esencialmente la misma que esta: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis
respondiendo a este comentario stackoverflow.com/questions/35328652/…
Shogg
0

Usar patrón observable. Puede poner el valor Observable (no Asunto) en el parámetro de entrada y administrarlo desde el componente principal. No necesita la función de devolución de llamada.

Ver ejemplo: https://stackoverflow.com/a/49662611/4604351

Alexey Baranoshnikov
fuente
¿Puedes ilustrarlo con un ejemplo de trabajo?
Michail Michailidis
0

Otra alternativa

El OP solicitó una forma de usar una devolución de llamada. En este caso, se refería específicamente a una función que procesa un evento (en su ejemplo: un evento de clic), que se tratará como la respuesta aceptada de @serginho sugiere: con @Outputy EventEmitter.

Sin embargo, hay una diferencia entre una devolución de llamada y un evento: con una devolución de llamada, su componente hijo puede recuperar algunos comentarios o información del padre, pero un evento solo puede informar que algo sucedió sin esperar ningún comentario.

Hay casos de uso donde es necesaria una retroalimentación, ej. obtener un color o una lista de elementos que el componente necesita manejar. Puede usar funciones enlazadas como sugieren algunas respuestas, o puede usar interfaces (esa es siempre mi preferencia).

Ejemplo

Supongamos que tiene un componente genérico que opera sobre una lista de elementos {id, name} que desea usar con todas las tablas de la base de datos que tienen estos campos. Este componente debe:

  • recuperar un rango de elementos (página) y mostrarlos en una lista
  • permitir eliminar un elemento
  • informar que se hizo clic en un elemento, para que el padre pueda tomar algunas acciones.
  • permitir recuperar la siguiente página de elementos.

Componente hijo

Usando el enlace normal necesitaríamos 1 @Input()y 3 @Output()parámetros (pero sin ningún comentario del padre). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, pero al crear una interfaz solo necesitaremos una @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 principal

Ahora podemos usar el componente de lista en el padre.

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

Tenga en cuenta que <list-ctrl>recibe this(componente principal) como el objeto de devolución de llamada. Una ventaja adicional es que no es necesario enviar la instancia principal, puede ser un servicio o cualquier objeto que implemente la interfaz si su caso de uso lo permite.

El ejemplo completo está en este stackblitz .

WPomier
fuente
-3

La respuesta actual se puede simplificar a ...

@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
fuente
entonces no hay necesidad de vincular explícitamente?
Michail Michailidis
3
Sin .bind(this)eso, el thisinterior de la devolución de llamada será lo windowque puede no importar dependiendo de su caso de uso. Sin embargo, si tiene thisalguna devolución de llamada, .bind(this)es necesario. Si no lo hace, esta versión simplificada es el camino a seguir.
SnareChops
3
Recomiendo siempre vincular la devolución de llamada con el componente, porque eventualmente lo usará thisdentro de la función de devolución de llamada. Es solo propenso a errores.
Alexandre Junges
Ese es un ejemplo de un antipatrón Angular 2.
Serginho
No tiene que ser un antipatrón. Hay casos en los que quieres exactamente esto. No es tan raro querer decirle al componente CÓMO hacer algo que no se trata de la vista. Tiene sentido y no veo por qué esta respuesta está recibiendo tanto odio.
Lazar Ljubenović