Funkcja zwrotnego przekazywania kątowego do komponentu potomnego jako @Input podobnie jak w AngularJS

227

AngularJS ma parametry i, w których można przekazać wywołanie zwrotne do dyrektywy (np. Sposób wywołania zwrotnego AngularJS . Czy możliwe jest przekazanie wywołania zwrotnego jako @Inputdla komponentu Angular (coś takiego jak poniżej)? Jeśli nie, co byłoby najbliższe temu AngularJS robi?

@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
źródło
6
dla przyszłych czytelników @Inputsposób zasugerował, że mój kod spagetti i nie jest łatwy w utrzymaniu .. @Outputs są znacznie bardziej naturalnym sposobem robienia tego, co chcę. W rezultacie zmieniłem przyjętą odpowiedź
Michail Michailidis,
@IanS pytanie dotyczy tego, jak coś się robi w Angular podobnym do AngularJS? dlaczego tytuł wprowadza w błąd?
Michail Michailidis
Angular różni się bardzo od AngularJS. Angular 2+ to po prostu Angular.
Ian S
1
Naprawiono twój tytuł;)
Ian S
1
@IanS Thanks! teraz pytanie dotyczy także angularJs - z dodanym tagiem.
Michail Michailidis,

Odpowiedzi:

296

Myślę, że to złe rozwiązanie. Jeśli chcesz przekazać funkcję do komponentu @Input(), @Output()dekorator jest tym, czego szukasz.

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
źródło
45
Mówiąc ściślej, nie przekazujesz funkcji, a raczej podłączasz detektor zdarzeń detektora do wyjścia. Pomocny w zrozumieniu, dlaczego to działa.
Jens
13
To świetna metoda, ale po przeczytaniu tej odpowiedzi pozostało mi wiele pytań. Miałem nadzieję, że będzie to bardziej dogłębne lub będzie mieć link opisujący @Outputi EventEmitter. Oto dokumentacja Angular dla @Output dla zainteresowanych.
WebWanderer
9
Jest to dobre w przypadku wiązania w jedną stronę. Możesz dołączyć do wydarzenia dziecka. Ale nie można przekazać funkcji wywołania zwrotnego do dziecka i pozwolić mu przeanalizować wartość zwrotną wywołania zwrotnego. Odpowiedź na to pozwala.
wieża
3
Oczekiwałbym, że będę miał więcej wyjaśnień na temat tego, dlaczego preferować jeden sposób w porównaniu do drugiego, zamiast „Myślę, że to złe rozwiązanie”.
Fidan Hakaj,
6
Prawdopodobnie dobre w 80% przypadków, ale nie wtedy, gdy komponent potomny chce wizualizacji, pod warunkiem istnienia wywołania zwrotnego.
John Freeman,
115

AKTUALIZACJA

Ta odpowiedź została przesłana, gdy Angular 2 był jeszcze w wersji alfa, a wiele funkcji było niedostępnych / nieudokumentowanych. Chociaż poniższe funkcje będą nadal działać, ta metoda jest teraz całkowicie nieaktualna. Ja zdecydowanie polecam zaakceptowane odpowiedź na niżej.

Oryginalna odpowiedź

Tak, w rzeczywistości tak jest, jednak należy upewnić się, że zakres jest prawidłowy. W tym celu skorzystałem z właściwości, aby upewnić się, że thisoznacza to, czego chcę.

@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
źródło
1
To działało! Dzięki! Chciałbym, żeby dokumentacja miała to gdzieś :)
Michail Michailidis
1
Możesz użyć metody statycznej, jeśli chcesz, ale wtedy nie będziesz miał dostępu do żadnego z elementów instancji składnika. Więc prawdopodobnie nie twój przypadek użycia. Ale tak, trzeba by to przekazać również odParent -> Child
SnareChops
3
Świetna odpowiedź! Jednak zwykle nie zmieniam nazwy funkcji podczas wiązania. w ngOnInitChciałbym po prostu użyć: this.theCallback = this.theCallback.bind(this)i wtedy możesz przekazać dalej theCallbackzamiast theBoundCallback.
Zack
1
@MichailMichailidis Tak, zgadzam się z twoim rozwiązaniem i zaktualizowałem moją odpowiedź notatką, aby poprowadzić ludzi na lepszy sposób. Dzięki za pilnowanie tego.
SnareChops
7
@Output i EventEmitter są w porządku dla wiązania jednokierunkowego. Możesz połączyć się ze zdarzeniem dziecka, ale nie możesz przekazać funkcji wywołania zwrotnego dziecku i pozwolić mu przeanalizować wartość zwrotną wywołania zwrotnego. Ta odpowiedź na to pozwala.
wieża
31

Alternatywa dla odpowiedzi udzielonej przez SnareChops.

Możesz użyć .bind (this) w swoim szablonie, aby uzyskać ten sam efekt. Może nie jest tak czysty, ale oszczędza kilka linii. Obecnie używam kątowej wersji 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
źródło
2
jak skomentowali inni, bind (this) w szablonie nie jest nigdzie udokumentowany, więc w przyszłości może stać się przestarzały / nieobsługiwany. Plus ponownie @Inputpowoduje, że kod staje się spaghetti, a stosowanie @Outputwyników w bardziej naturalny / nieplątany proces
Michail Michailidis
1
Kiedy umieścisz bind () w szablonie, Angular ponownie ocenia to wyrażenie przy każdym wykryciu zmiany. Drugie rozwiązanie - wiązanie poza szablonem - jest mniej zwięzłe, ale nie ma tego problemu.
Chris
pytanie: robiąc .bind (this), wiążesz metodę theCallBack z dzieckiem lub rodzicem? Myślę, że to z dzieckiem. Ale rzecz w tym, że gdy wywoływane jest powiązanie, zawsze dzwoni to dziecko, więc to powiązanie nie wydaje się konieczne, jeśli mam rację.
ChrisZ
Wiąże się ze składnikiem nadrzędnym. Powodem tego jest to, że gdy wywoływana jest funkcja CallCack (), prawdopodobnie będzie chciała zrobić coś wewnątrz siebie, a jeśli „to” nie jest komponentem nadrzędnym, będzie poza kontekstem i dlatego nie będzie w stanie dotrzeć do własnych metod i zmiennych już.
Max Fahl
29

W niektórych przypadkach może być konieczne wykonanie logiki biznesowej przez komponent nadrzędny. W poniższym przykładzie mamy komponent potomny, który renderuje wiersz tabeli w zależności od logiki dostarczonej przez komponent nadrzędny:

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

Chciałem więc tutaj zademonstrować 2 rzeczy:

  1. Funkcje Fat Arrow (=>) zamiast .bind (this) utrzymują właściwy kontekst;
  2. Typesafe deklaracja funkcji zwrotnej w komponencie potomnym.
Danylo Zatorsky
źródło
1
Świetne wytłumaczenie użycia grubej strzały do ​​zastąpienia użycia.bind(this)
TYMG
6
Wskazówka dotycząca użytkowania: Pamiętaj, aby umieścić [getRowColor]="getColor"i nie [getRowColor]="getColor()";-)
wstawiłeś,
Miły. Właśnie tego szukałem. Prosty i skuteczny.
BrainSlugs83
7

Jako przykład używam okna modalnego logowania, gdzie okno modalne jest nadrzędne, formularz logowania to dziecko, a przycisk logowania wywołuje funkcję zamknięcia modalnego rodzica.

Modal nadrzędny zawiera funkcję zamykania modalu. Ten rodzic przekazuje funkcję zamykania do komponentu potomnego logowania.

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

Po przesłaniu formularza podrzędnego logowania dziecko zamyka moduł nadrzędny za pomocą funkcji wywołania zwrotnego nadrzędnego

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
źródło
7

Alternatywa dla odpowiedzi udzielonej przez Maxa Fahla.

Możesz zdefiniować funkcję wywołania zwrotnego jako funkcję strzałki w komponencie nadrzędnym, aby nie trzeba było tego wiązać.

@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
źródło
5

Przekazywanie metody z argumentem za pomocą .bind w szablonie

@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
źródło
Czy twoja odpowiedź nie jest zasadniczo taka sama: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis
odpowiadając na ten komentarz stackoverflow.com/questions/35328652/…
Shogg
0

Użyj obserwowalnego wzoru. Możesz wstawić wartość Obserwowalną (nie Temat) do parametru wejściowego i zarządzać nią z komponentu nadrzędnego. Nie potrzebujesz funkcji oddzwaniania.

Zobacz przykład: https://stackoverflow.com/a/49662611/4604351

Alexey Baranoshnikov
źródło
czy możesz to zilustrować działającym przykładem?
Michail Michailidis,
0

Kolejna alternatywa.

OP poprosił o sposób oddzwonienia. W tym przypadku odnosił się konkretnie do funkcji przetwarzającej zdarzenie (w jego przykładzie: zdarzenie kliknięcia), która powinna być traktowana jako zaakceptowana odpowiedź z @serginho: za pomocą @OutputiEventEmitter .

Istnieje jednak różnica między wywołaniem zwrotnym a zdarzeniem: dzięki wywołaniu zwrotnemu komponent podrzędny może uzyskać informacje zwrotne lub informacje od rodzica, ale zdarzenie może tylko poinformować, że coś się wydarzyło bez oczekiwania na informację zwrotną.

Istnieją przypadki użycia, w których konieczne jest sprzężenie zwrotne, np. uzyskać kolor lub listę elementów, które komponent musi obsłużyć. Możesz użyć funkcji powiązanych, jak sugerują niektóre odpowiedzi, lub możesz użyć interfejsów (to zawsze moja preferencja).

Przykład

Załóżmy, że masz ogólny komponent, który działa na liście elementów {id, name}, których chcesz używać ze wszystkimi tabelami bazy danych zawierającymi te pola. Ten komponent powinien:

  • pobierz zakres elementów (stronę) i pokaż je na liście
  • zezwól na usunięcie elementu
  • informuje, że element został kliknięty, aby rodzic mógł podjąć pewne działania.
  • zezwól na pobranie następnej strony elementów.

Komponent potomny

Używając normalnego wiązania potrzebowalibyśmy 1 @Input()i 3 @Output()parametry (ale bez żadnej informacji zwrotnej od rodzica). Dawny. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, ale do utworzenia interfejsu potrzebujemy tylko jednego @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); 
    }
}

Komponent macierzysty

Teraz możemy użyć komponentu listy w elemencie nadrzędnym.

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

Zauważ, że <list-ctrl>odbierathis (komponent nadrzędny) jako obiekt wywołania zwrotnego. Dodatkową zaletą jest to, że nie jest wymagane wysyłanie instancji nadrzędnej, może to być usługa lub dowolny obiekt implementujący interfejs, jeśli pozwala na to przypadek użycia.

Kompletny przykład jest na tym stackblitz .

WPomier
źródło
-3

Obecną odpowiedź można uprościć ...

@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; 
  ...
}
niebieski
źródło
więc nie ma potrzeby jawnego wiązania?
Michail Michailidis
3
Bez .bind(this)tego thiswnętrze oddzwaniania będzie, windowco może nie mieć znaczenia w zależności od przypadku użycia. Jeśli jednak w ogóle masz thisoddzwonienie, .bind(this)jest to konieczne. Jeśli nie, to ta uproszczona wersja jest najlepszym rozwiązaniem.
SnareChops
3
Zalecam, aby zawsze wiązać wywołanie zwrotne ze składnikiem, ponieważ w końcu będzie można użyć thisfunkcji wywołania zwrotnego. To po prostu podatne na błędy.
Alexandre Junges,
To przykład antipatternu Angular 2.
Serginho,
To nie musi być anty-wzór. Są przypadki, w których chcesz tego dokładnie. Często zdarza się, że chcesz powiedzieć komponentowi JAK zrobić coś, co nie dotyczy widoku. To ma sens i nie rozumiem, dlaczego ta odpowiedź jest tak bardzo nienawidzona.
Lazar Ljubenović