Angular Pass-Rückruffunktion an untergeordnete Komponente als @Input ähnlich wie bei AngularJS

226

AngularJS verfügt über die & -Parameter, mit denen Sie einen Rückruf an eine Direktive übergeben können (z. B. AngularJS-Methode für Rückrufe . Ist es möglich, einen Rückruf als @Inputfür eine Angular-Komponente zu übergeben (so etwas wie unten)? Wenn nicht, was wäre dem am nächsten? AngularJS macht?

@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
quelle
6
Für zukünftige Leser hat der @Inputvorgeschlagene Weg meine Code-Spagetti gemacht und ist nicht einfach zu pflegen. @Outputs sind eine viel natürlichere Art, das zu tun, was ich will. Infolgedessen änderte ich die akzeptierte Antwort
Michail Michailidis
@ IanS Frage ist, wie etwas in Angular ähnlich wie AngularJS gemacht wird? Warum ist der Titel irreführend?
Michail Michailidis
Angular unterscheidet sich stark von AngularJS. Angular 2+ ist nur Angular.
Ian S
1
Ihr Titel wurde korrigiert;)
Ian S
1
@ IanS Danke! Jetzt geht es aber auch um AngularJs - mit dem Tag, das Sie hinzugefügt haben.
Michail Michailidis

Antworten:

294

Ich denke das ist eine schlechte Lösung. Wenn Sie eine Funktion an eine Komponente mit übergeben möchten @Input(), ist @Output()Decorator genau das, wonach Sie suchen.

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
quelle
45
Um genau zu sein, übergeben Sie die Funktion nicht, sondern schließen einen Listener-Ereignis-Listener an die Ausgabe an. Hilfreich, um zu verstehen, warum es funktioniert.
Jens
13
Dies ist eine großartige Methode, aber nachdem ich diese Antwort gelesen hatte, blieben mir viele Fragen. Ich hatte gehofft, es würde ausführlicher sein oder einen Link zur Beschreibung @Outputund EventEmitter. Hier ist die Angular-Dokumentation für @Output für Interessierte.
WebWanderer
9
Dies ist gut für die Einwegbindung. Sie können sich an das Ereignis des Kindes anschließen. Sie können dem untergeordneten Element jedoch keine Rückruffunktion übergeben und den Rückgabewert des Rückrufs analysieren lassen. Die Antwort unten erlaubt das.
Turm
3
Ich würde erwarten, mehr Erklärungen darüber zu haben, warum man einen Weg dem anderen vorzieht, anstatt "Ich denke, das ist eine schlechte Lösung."
Fidan Hakaj
6
Wahrscheinlich gut für 80% der Fälle, aber nicht, wenn eine untergeordnete Komponente eine Visualisierung wünscht, die davon abhängig ist, ob ein Rückruf vorhanden ist.
John Freeman
115

AKTUALISIEREN

Diese Antwort wurde übermittelt, als Angular 2 noch in Alpha war und viele der Funktionen nicht verfügbar / nicht dokumentiert waren. Während das Folgende noch funktioniert, ist diese Methode jetzt völlig veraltet. Ich empfehle dringend die akzeptierte Antwort über die unten.

Ursprüngliche Antwort

Ja, in der Tat, aber Sie sollten sicherstellen, dass der Umfang korrekt ist. Dafür habe ich eine Eigenschaft verwendet, um sicherzustellen, dass dies thisbedeutet, was ich möchte.

@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
quelle
1
Das hat funktioniert! Vielen Dank! Ich wünschte, die Dokumentation hätte das irgendwo :)
Michail Michailidis
1
Sie könnten eine statische Methode verwenden, wenn Sie möchten, aber dann hätten Sie keinen Zugriff auf eines der Instanzmitglieder der Komponente. Also wahrscheinlich nicht dein Anwendungsfall. Aber ja, das müssten Sie auch vonParent -> Child
SnareChops
3
Gute Antwort! Normalerweise benenne ich die Funktion beim Binden nicht um. in ngOnInitwürde ich nur verwenden: this.theCallback = this.theCallback.bind(this)und dann kannst du theCallbackstatt weitergeben theBoundCallback.
Zack
1
@MichailMichailidis Ja, ich stimme Ihrer Lösung zu und habe meine Antwort mit einem Hinweis aktualisiert, um die Menschen auf den besseren Weg zu führen. Vielen Dank, dass Sie diesen im Auge behalten.
SnareChops
7
@Output und EventEmitter eignen sich gut für die Einwegbindung. Sie können sich mit dem Ereignis des Kindes verbinden, aber Sie können dem Kind keine Rückruffunktion übergeben und es den Rückgabewert des Rückrufs analysieren lassen. Diese Antwort erlaubt das.
Turm
31

Eine Alternative zur Antwort von SnareChops.

Sie können .bind (this) in Ihrer Vorlage verwenden, um den gleichen Effekt zu erzielen. Es ist vielleicht nicht so sauber, aber es spart ein paar Zeilen. Ich bin derzeit auf Winkel 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
quelle
2
Wie andere kommentiert haben, ist bind (this) in der Vorlage nirgends dokumentiert, sodass es in Zukunft möglicherweise veraltet / nicht mehr unterstützt wird. Plus @Inputbewirkt erneut, dass der Code zu Spaghetti wird und die Verwendung @Outputzu einem natürlicheren / entwirrten Prozess führt
Michail Michailidis
1
Wenn Sie bind () in die Vorlage einfügen, wertet Angular diesen Ausdruck bei jeder Änderungserkennung neu aus. Die andere Lösung - das Binden außerhalb der Vorlage - ist weniger präzise, ​​hat dieses Problem jedoch nicht.
Chris
Frage: Wenn Sie .bind (this) ausführen, binden Sie die Methode theCallBack mit dem Kind oder Elternteil? Ich denke, es ist mit dem Kind. Aber die Sache ist, wenn die Bindung aufgerufen wird, ist es immer das Kind, das sie aufruft, so dass diese Bindung nicht notwendig erscheint, wenn ich richtig bin.
ChrisZ
Es bindet an die übergeordnete Komponente. Der Grund dafür ist, dass theCallBack () beim Aufruf wahrscheinlich etwas in sich selbst tun möchte. Wenn "this" nicht die übergeordnete Komponente ist, liegt es außerhalb des Kontexts und kann daher seine eigenen Methoden und Variablen nicht erreichen nicht mehr.
Max Fahl
28

In einigen Fällen muss die Geschäftslogik möglicherweise von einer übergeordneten Komponente ausgeführt werden. Im folgenden Beispiel haben wir eine untergeordnete Komponente, die Tabellenzeilen abhängig von der von der übergeordneten Komponente bereitgestellten Logik rendert:

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

Also wollte ich hier zwei Dinge demonstrieren:

  1. Der fette Pfeil (=>) funktioniert anstelle von .bind (this), um den richtigen Kontext zu halten.
  2. Typesichere Deklaration einer Rückruffunktion in der untergeordneten Komponente.
Danylo Zatorsky
quelle
1
.bind(this)
Gute
6
Usage Tipp: Achten Sie darauf , setzen [getRowColor]="getColor"und nicht [getRowColor]="getColor()";-)
Simon_Weaver
Nett. Genau das habe ich gesucht. Einfach und effektiv.
BrainSlugs83
7

Als Beispiel verwende ich ein modales Anmeldefenster, in dem das modale Fenster das übergeordnete Element, das Anmeldeformular das untergeordnete Fenster und die Anmeldeschaltfläche die Schließfunktion des modalen übergeordneten Elements aufruft.

Das übergeordnete Modal enthält die Funktion zum Schließen des Modals. Dieses übergeordnete Element übergibt die Funktion zum Schließen an die untergeordnete Anmeldekomponente.

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

Nachdem die untergeordnete Anmeldekomponente das Anmeldeformular gesendet hat, schließt sie das übergeordnete Modal mithilfe der Rückruffunktion des übergeordneten Elements

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

Eine Alternative zu der Antwort, die Max Fahl gab.

Sie können die Rückruffunktion als Pfeilfunktion in der übergeordneten Komponente definieren, damit Sie diese nicht binden müssen.

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

Übergabe der Methode mit Argument unter Verwendung von .bind in der Vorlage

@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
quelle
Ist Ihre Antwort nicht im Wesentlichen dieselbe wie diese: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis
Beantwortung dieses Kommentars stackoverflow.com/questions/35328652/…
Shogg
0

Verwenden Sie das beobachtbare Muster. Sie können den Wert Observable (nicht Subject) in den Eingabeparameter einfügen und über die übergeordnete Komponente verwalten. Sie benötigen keine Rückruffunktion.

Siehe Beispiel: https://stackoverflow.com/a/49662611/4604351

Alexey Baranoshnikov
quelle
Können Sie es bitte anhand eines Arbeitsbeispiels veranschaulichen?
Michail Michailidis
0

Eine andere Alternative.

Das OP fragte nach einer Möglichkeit, einen Rückruf zu verwenden. In diesem Fall bezog er sich speziell auf eine Funktion, die ein Ereignis verarbeitet (in seinem Beispiel: ein Klickereignis), die wie die von @serginho akzeptierte Antwort behandelt werden soll: mit @Outputund EventEmitter.

Es gibt jedoch einen Unterschied zwischen einem Rückruf und einem Ereignis: Mit einem Rückruf kann Ihre untergeordnete Komponente Feedback oder Informationen vom übergeordneten Element abrufen, ein Ereignis kann jedoch nur darüber informieren, dass etwas passiert ist, ohne dass ein Feedback erwartet wird.

Es gibt Anwendungsfälle, in denen eine Rückmeldung erforderlich ist, z. Holen Sie sich eine Farbe oder eine Liste von Elementen, die die Komponente verarbeiten muss. Sie können gebundene Funktionen verwenden, wie einige Antworten vorgeschlagen haben, oder Sie können Schnittstellen verwenden (das ist immer meine Präferenz).

Beispiel

Angenommen, Sie haben eine generische Komponente, die eine Liste von Elementen {id, name} verarbeitet, die Sie für alle Datenbanktabellen mit diesen Feldern verwenden möchten. Diese Komponente sollte:

  • Rufen Sie eine Reihe von Elementen (Seite) ab und zeigen Sie sie in einer Liste an
  • Erlaube das Entfernen eines Elements
  • Informieren Sie, dass auf ein Element geklickt wurde, damit das übergeordnete Element einige Aktionen ausführen kann.
  • Ermöglichen das Abrufen der nächsten Seite mit Elementen.

Untergeordnete Komponente

Bei normaler Bindung benötigen wir 1 @Input()und 3 @Output()Parameter (jedoch ohne Rückmeldung des Elternteils). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, aber für die Erstellung einer Schnittstelle benötigen wir nur eine @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); 
    }
}

Übergeordnete Komponente

Jetzt können wir die Listenkomponente im übergeordneten Element verwenden.

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

Beachten Sie, dass der <list-ctrl>Empfang this(übergeordnete Komponente) das Rückrufobjekt ist. Ein zusätzlicher Vorteil besteht darin, dass die übergeordnete Instanz nicht gesendet werden muss. Es kann sich um einen Dienst oder ein Objekt handeln, das die Schnittstelle implementiert, sofern Ihr Anwendungsfall dies zulässt.

Das vollständige Beispiel finden Sie auf diesem Stackblitz .

WPomier
quelle
-3

Die aktuelle Antwort kann vereinfacht werden, um ...

@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; 
  ...
}
Blau
quelle
Es besteht also keine Notwendigkeit, explizit zu binden?
Michail Michailidis
3
Ohne das .bind(this)dann ist das thisInnere des Rückrufs, windowwas je nach Anwendungsfall möglicherweise keine Rolle spielt. Wenn Sie jedoch überhaupt einen thisRückruf haben, .bind(this)ist dies erforderlich. Wenn Sie dies nicht tun, ist diese vereinfachte Version der richtige Weg.
SnareChops
3
Ich empfehle, den Rückruf immer an die Komponente zu binden, da Sie ihn schließlich thisinnerhalb der Rückruffunktion verwenden werden. Es ist nur fehleranfällig.
Alexandre Junges
Das ist ein Beispiel für ein Angular 2-Antimuster.
Serginho
Es muss kein Anti-Muster sein. Es gibt Fälle, in denen Sie genau dies möchten. Es ist nicht ungewöhnlich, der Komponente mitteilen zu wollen, wie etwas zu tun ist, bei dem es nicht um die Ansicht geht. Es macht Sinn und ich verstehe nicht, warum diese Antwort so viel Hass bekommt.
Lazar Ljubenović