Funzione di callback del passaggio angolare al componente figlio come @Input simile al modo AngularJS


227

AngularJS ha i parametri & in cui è possibile passare un callback a una direttiva (es. Modo AngularJS di callback . È possibile passare un callback come @Inputper un componente angolare (qualcosa come sotto)? Se non quale sarebbe la cosa più vicina a ciò che AngularJS fa?

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

6
per i futuri lettori il @Inputmodo suggerito ha reso il mio codice spagetti e non facile da mantenere .. @Outputs sono un modo molto più naturale di fare quello che voglio. Di conseguenza ho cambiato la risposta accettata
Michail Michailidis,

La domanda di @IanS riguarda come viene fatto qualcosa in angolare simile a AngularJS? perché il titolo è fuorviante?
Michail Michailidis,

Angular è molto diverso da AngularJS. Angular 2+ è solo angolare.
Ian S,

1
Risolto il tuo titolo;)
Ian S

1
@IanS Grazie! ora la domanda riguarda anche angularJs - con il tag che hai aggiunto però.
Michail Michailidis,

Risposte:


296

Penso che sia una cattiva soluzione. Se vuoi passare una funzione nel componente con @Input(), @Output()decoratore è quello che stai cercando.

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>

45
Per essere precisi, non stai passando la funzione, ma piuttosto allacci un listener di eventi listener all'output. Utile per capire perché funziona.
Jens,

13
Questo è un ottimo metodo, ma mi sono rimaste molte domande dopo aver letto questa risposta. Speravo che fosse più approfondito o che fosse fornito un collegamento che descriva @Outpute EventEmitter. Quindi, ecco la documentazione angolare per @Output per chi è interessato.
WebWanderer,

9
Questo va bene per l'associazione unidirezionale. Puoi collegarti all'evento del bambino. Ma non è possibile passare una funzione di richiamata al figlio e lasciarlo analizzare il valore di ritorno della richiamata. La risposta qui sotto lo consente.
rook

3
Mi aspetterei di avere ulteriori spiegazioni sul perché preferire un modo rispetto all'altro invece di avere "Penso che sia una cattiva soluzione".
Fidan Hakaj,

6
Probabilmente buono per l'80% dei casi, ma non quando un componente figlio vuole che la visualizzazione sia subordinata all'esistenza di un callback.
John Freeman,

115

AGGIORNARE

Questa risposta è stata inviata quando Angular 2 era ancora in modalità alfa e molte delle funzionalità non erano disponibili / non documentate. Mentre il seguito continuerà a funzionare, questo metodo è ora completamente obsoleto. Consiglio vivamente la risposta accettata sopra.

Risposta originale

Sì, in effetti lo è, tuttavia ti consigliamo di assicurarti che sia impostato correttamente. Per questo ho usato una proprietà per assicurarmi che thissignifichi quello che voglio.

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

1
Questo ha funzionato! Grazie! Vorrei che la documentazione ce l'avesse da qualche parte :)
Michail Michailidis l'

1
Se lo desideri, puoi utilizzare un metodo statico, ma non avresti accesso a nessuno dei membri dell'istanza del componente. Quindi probabilmente non è il tuo caso d'uso. Ma sì, dovresti passare anche quello daParent -> Child
SnareChops l'

3
Bella risposta! In genere, tuttavia, non rinominare la funzione durante l'associazione. in ngOnInitvorrei solo usare: this.theCallback = this.theCallback.bind(this)e quindi puoi passare theCallbackinvece di theBoundCallback.
Zack,

1
@MichailMichailidis Sì, sono d'accordo con la tua soluzione e ho aggiornato la mia risposta con una nota per guidare le persone nel modo migliore. Grazie per tenere d'occhio questo.
SnareChops,

7
@Output ed EventEmitter vanno bene per l'associazione unidirezionale. È possibile collegarsi all'evento del figlio ma non è possibile passare una funzione di richiamata al figlio e lasciarlo analizzare il valore di ritorno della richiamata. Questa risposta lo consente.
rook

31

Un'alternativa alla risposta data da SnareChops.

Puoi usare .bind (questo) nel tuo modello per avere lo stesso effetto. Potrebbe non essere così pulito ma salva un paio di righe. Sono attualmente in angolare 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; 
  ...
}

2
come altri hanno commentato bind (questo) nel modello non è documentato da nessuna parte, quindi potrebbe diventare deprecato / non supportato in futuro. Inoltre, @Inputil codice diventa spaghetti e l'utilizzo dei @Outputrisultati in un processo più naturale / districato
Michail Michailidis,

1
Quando si inserisce bind () nel modello, Angular rivaluta questa espressione ad ogni rilevamento di modifica. L'altra soluzione - eseguire il bind all'esterno del modello - è meno concisa, ma non presenta questo problema.
Chris,

domanda: quando si esegue .bind (questo), si sta vincolando il metodo CallBack con il figlio o il genitore? Penso che sia con il bambino. Ma il fatto è che quando viene chiamato il bind, è sempre il bambino a chiamarlo, quindi questo bind non sembra necessario se ho ragione.
ChrisZ,

Si lega al componente padre. Il motivo è che quando viene chiamato theCallBack (), probabilmente vorrà fare qualcosa dentro di sé, e se "questo" non è il componente genitore sarà fuori contesto e quindi non può raggiungere i suoi metodi e variabili più.
Max Fahl,

29

In alcuni casi, potrebbe essere necessario che la logica aziendale venga eseguita da un componente padre. Nell'esempio seguente abbiamo un componente figlio che esegue il rendering della riga della tabella in base alla logica fornita dal componente padre:

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

Quindi, volevo dimostrare 2 cose qui:

  1. La freccia grassa (=>) funziona invece di .bind (this) per contenere il giusto contesto;
  2. Dichiarazione typesafe di una funzione di callback nel componente figlio.

1
Grande spiegazione per l'uso della freccia grassa per sostituire l'uso di.bind(this)
TYMG

6
[getRowColor]="getColor"[getRowColor]="getColor()"
Suggerimento d'

Bello. Questo e 'esattamente quello che stavo cercando. Semplice ed efficace
BrainSlugs83,

7

Ad esempio, sto usando una finestra modale di accesso, in cui la finestra modale è il genitore, il modulo di accesso è il figlio e il pulsante di accesso richiama la funzione di chiusura del genitore modale.

Il modale padre contiene la funzione per chiudere il modale. Questo genitore passa la funzione di chiusura al componente figlio di accesso.

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

Dopo che il componente di accesso figlio ha inviato il modulo di accesso, chiude il modale padre utilizzando la funzione di richiamata del padre

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

7

Un'alternativa alla risposta di Max Fahl.

È possibile definire la funzione di richiamata come una funzione freccia nel componente padre in modo da non doverlo associare.

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


5

Passaggio del metodo con argomento, utilizzando .bind all'interno del modello

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

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

  @Input()
  public action: Function; 

  ...
}

La tua risposta non è essenzialmente la stessa di questa: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis,

rispondere a questo commento stackoverflow.com/questions/35328652/...
Shogg


0

Un'altra alternativa

L'OP ha chiesto un modo per utilizzare un callback. In questo caso si riferiva specificamente a una funzione che elabora un evento (nel suo esempio: un evento click), che deve essere trattato come la risposta accettata da @serginho suggerisce: con @Outpute EventEmitter.

Tuttavia, esiste una differenza tra un callback e un evento: con un callback il componente figlio può recuperare alcuni feedback o informazioni dal genitore, ma un evento può solo informare che qualcosa è accaduto senza aspettarsi alcun feedback.

Ci sono casi d'uso in cui è necessario un feedback, ad es. ottenere un colore o un elenco di elementi che il componente deve gestire. Puoi usare le funzioni associate come suggerito da alcune risposte, oppure puoi usare le interfacce (questa è sempre la mia preferenza).

Esempio

Supponiamo che tu abbia un componente generico che opera su un elenco di elementi {id, name} che desideri utilizzare con tutte le tabelle del tuo database che hanno questi campi. Questo componente dovrebbe:

  • recuperare una serie di elementi (pagina) e mostrarli in un elenco
  • consenti di rimuovere un elemento
  • informa che è stato fatto clic su un elemento, in modo che il genitore possa intraprendere alcune azioni.
  • consenti recuperare la pagina successiva di elementi.

Componente figlio

Usando l'associazione normale avremmo bisogno di 1 @Input()e 3 @Output()parametri (ma senza alcun feedback da parte del genitore). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, ma creando un'interfaccia ne avremo bisogno solo 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 principale

Ora possiamo usare il componente elenco nel genitore.

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

Si noti che <list-ctrl>riceve this(componente principale) come oggetto callback. Un ulteriore vantaggio è che non è necessario inviare l'istanza padre, può essere un servizio o qualsiasi oggetto che implementa l'interfaccia se il caso d'uso lo consente.

L'esempio completo è su questo stackblitz .


-3

La risposta corrente può essere semplificata per ...

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

quindi non c'è bisogno di legarsi esplicitamente?
Michail Michailidis,

3
Senza la .bind(this)parte thisinterna del callback sarà windowche potrebbe non importare a seconda del caso d'uso. Tuttavia, se si dispone thisdel callback, .bind(this)è necessario. In caso contrario, questa versione semplificata è la strada da percorrere.
SnareChops

3
Consiglio di associare sempre il callback con il componente, perché alla fine userete thisall'interno della funzione di callback. È solo soggetto a errori.
Alexandre Junges,

Questo è un esempio di un antipasto di Angular 2.
Serginho,

Non deve essere un anti-schema. Ci sono casi in cui vuoi esattamente questo. Non è insolito voler dire al componente COME fare qualcosa che non riguarda la vista. Ha senso e non vedo perché questa risposta stia ottenendo così tanto odio.
Lazar Ljubenović,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.