Fungsi callback pass sudut untuk komponen anak sebagai @Input mirip dengan cara AngularJS

227

AngularJS memiliki & parameter di mana Anda bisa meneruskan panggilan balik ke arahan (misalnya cara panggilan balik AngularJS . Apakah mungkin untuk meneruskan panggilan balik sebagai @Inputuntuk Komponen Angular (seperti di bawah ini)? Jika tidak, apa yang akan menjadi hal terdekat dengan apa AngularJS tidak?

@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
sumber
6
untuk pembaca di masa depan @Inputcara yang disarankan membuat spagetti kode saya dan tidak mudah dipelihara .. @Outputs adalah cara yang jauh lebih alami untuk melakukan apa yang saya inginkan. Akibatnya saya mengubah jawaban yang diterima
Michail Michailidis
@IanS pertanyaannya adalah tentang bagaimana sesuatu dilakukan di Angular mirip dengan AngularJS? mengapa judul itu menyesatkan?
Michail Michailidis
Angular sangat berbeda dari AngularJS. Angular 2+ hanyalah Angular.
Ian S
1
Memperbaiki judul Anda;)
Ian S
1
@Ian, terima kasih! sekarang pertanyaannya adalah tentang angularJs juga - dengan tag yang Anda tambahkan sekalipun.
Michail Michailidis

Jawaban:

296

Saya pikir itu solusi yang buruk. Jika Anda ingin memasukkan Fungsi ke dalam komponen @Input(), @Output()dekorator adalah yang Anda cari.

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
sumber
45
Tepatnya Anda tidak melewati fungsi melainkan menghubungkan pendengar acara pendengar ke output. Bermanfaat untuk memahami mengapa ini berhasil.
Jens
13
Ini adalah metode yang hebat, tetapi saya dibiarkan dengan banyak pertanyaan setelah membaca jawaban ini. Saya berharap akan lebih mendalam atau memiliki tautan yang disediakan untuk menjelaskan @Outputdan EventEmitter. Jadi, di sini adalah dokumentasi Angular untuk @Output bagi mereka yang tertarik.
WebWanderer
9
Ini bagus untuk pengikatan satu arah. Anda dapat terhubung ke acara anak. Tetapi Anda tidak bisa meneruskan fungsi panggilan balik ke anak dan membiarkannya menganalisis nilai pengembalian panggilan balik itu. Jawaban di bawah memungkinkan.
Benteng
3
Saya akan berharap memiliki lebih banyak penjelasan tentang mengapa lebih memilih satu cara vs yang lain daripada memiliki "Saya pikir itu solusi yang buruk.".
Fidan Hakaj
6
Mungkin bagus untuk 80% kasus, tetapi tidak ketika komponen anak menginginkan visualisasi tergantung pada apakah ada panggilan balik.
John Freeman
115

MEMPERBARUI

Jawaban ini diajukan ketika Angular 2 masih dalam alfa dan banyak fitur tidak tersedia / tidak terdokumentasi. Meskipun di bawah ini masih berfungsi, metode ini sekarang sepenuhnya usang. Saya sangat merekomendasikan jawaban yang diterima di bawah ini.

Jawaban Asli

Ya pada kenyataannya, namun Anda ingin memastikan bahwa itu dicakup dengan benar. Untuk ini saya telah menggunakan properti untuk memastikan itu thisberarti apa yang saya inginkan.

@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
sumber
1
Ini berhasil! Terima kasih! Saya berharap dokumentasi ada di suatu tempat :)
Michail Michailidis
1
Anda bisa menggunakan metode statis jika Anda mau, tetapi kemudian Anda tidak akan memiliki akses ke anggota instance komponen. Jadi mungkin bukan kasus penggunaan Anda. Tapi ya, Anda harus lulus juga dariParent -> Child
SnareChops
3
Jawaban bagus! Saya biasanya tidak mengganti nama fungsi saat mengikat. di ngOnInitSaya hanya akan menggunakan: this.theCallback = this.theCallback.bind(this)dan kemudian Anda dapat menyampaikan theCallbackbukan theBoundCallback.
Zack
1
@MichailMichailidis Ya, saya setuju dengan solusi Anda dan telah memperbarui jawaban saya dengan catatan untuk mengarahkan orang ke cara yang lebih baik. Terima kasih telah mengawasi yang satu ini.
SnareChops
7
@Output dan EventEmitter baik-baik saja untuk satu cara mengikat. Anda dapat terhubung ke acara anak tetapi Anda tidak bisa meneruskan fungsi panggilan balik ke anak dan membiarkannya menganalisis nilai pengembalian panggilan balik itu. Jawaban ini memungkinkan itu.
Benteng
31

Alternatif jawaban yang diberikan SnareChops.

Anda dapat menggunakan .bind (ini) di templat Anda untuk memiliki efek yang sama. Mungkin tidak bersih tetapi menghemat beberapa baris. Saya saat ini pada sudut 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
sumber
2
karena orang lain telah berkomentar mengikat (ini) dalam template tidak didokumentasikan sehingga mungkin menjadi usang / tidak didukung di masa depan. Plus lagi @Inputmenyebabkan kode menjadi spaghetti dan menggunakan @Outputhasil dalam proses yang lebih alami / tidak terurai
Michail Michailidis
1
Saat Anda menempatkan bind () di templat, Angular mengevaluasi ulang ekspresi ini di setiap deteksi perubahan. Solusi lain - melakukan ikatan di luar template - kurang ringkas, tetapi tidak memiliki masalah ini.
Chris
pertanyaan: ketika melakukan .bind (ini), Anda mengikat metode theCallBack dengan anak atau orang tua? Saya pikir itu dengan anak itu. Tapi masalahnya, ketika ikatan dipanggil, selalu anak yang memanggilnya, jadi ikatan ini sepertinya tidak perlu jika aku benar.
ChrisZ
Itu mengikat dengan komponen induk. Alasan mengapa hal ini dilakukan adalah ketika theCallBack () dipanggil, ia mungkin ingin melakukan sesuatu di dalam dirinya sendiri, dan jika "ini" bukan komponen induk maka akan keluar dari konteks dan oleh karena itu tidak dapat mencapai metode dan variabel sendiri. lagi.
Max Fahl
29

Dalam beberapa kasus, Anda mungkin perlu logika bisnis dilakukan oleh komponen induk. Dalam contoh di bawah ini kami memiliki komponen turunan yang merender baris tabel bergantung pada logika yang disediakan oleh komponen induk:

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

Jadi, saya ingin menunjukkan 2 hal di sini:

  1. Panah gemuk (=>) berfungsi alih-alih .bind (ini) untuk menahan konteks yang benar;
  2. Deklarasi Typesafe dari fungsi callback dalam komponen anak.
Danylo Zatorsky
sumber
1
Penjelasan hebat untuk penggunaan panah gemuk untuk menggantikan penggunaan.bind(this)
TYMG
6
Kiat penggunaan: Pastikan untuk meletakkan [getRowColor]="getColor"dan tidak [getRowColor]="getColor()";-)
Simon_Weaver
Bagus. Ini persis apa yang saya cari. Sederhana & efektif.
BrainSlugs83
7

Sebagai contoh, saya menggunakan jendela modal masuk, di mana jendela modal adalah orangtua, formulir login adalah anak dan tombol masuk memanggil kembali ke fungsi tutup modal induk.

Modal induk berisi fungsi untuk menutup modal. Orang tua ini meneruskan fungsi tutup ke komponen anak login.

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

Setelah komponen login anak mengirimkan formulir login, itu menutup modal induk menggunakan fungsi panggilan balik orangtua

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

Alternatif jawaban yang diberikan Max Fahl.

Anda dapat mendefinisikan fungsi panggilan balik sebagai fungsi panah di komponen induk sehingga Anda tidak perlu mengikatnya.

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

Melewati metode dengan argumen, menggunakan .bind di dalam template

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

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

  @Input()
  public action: Function; 

  ...
}
Bidik
sumber
Bukankah jawaban Anda pada dasarnya sama dengan ini: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis
menjawab komentar ini stackoverflow.com/questions/35328652/…
Shogg
0

Gunakan pola yang bisa diamati. Anda dapat memasukkan nilai yang Dapat Diamati (bukan Subjek) ke dalam parameter Input dan mengelolanya dari komponen induk. Anda tidak perlu fungsi panggilan balik.

Lihat contoh: https://stackoverflow.com/a/49662611/4604351

Alexey Baranoshnikov
sumber
dapatkah Anda menggambarkannya dengan contoh kerja?
Michail Michailidis
0

Alternatif lain.

OP menanyakan cara untuk menggunakan panggilan balik. Dalam hal ini ia merujuk secara khusus ke fungsi yang memproses suatu peristiwa (dalam contohnya: peristiwa klik), yang akan diperlakukan sebagai jawaban yang diterima dari @serginho menyarankan: dengan @OutputdanEventEmitter .

Namun, ada perbedaan antara panggilan balik dan peristiwa: Dengan panggilan balik komponen anak Anda dapat mengambil beberapa umpan balik atau informasi dari orang tua, tetapi suatu peristiwa hanya dapat menginformasikan bahwa sesuatu terjadi tanpa mengharapkan umpan balik.

Ada kasus penggunaan di mana umpan balik diperlukan, mis. dapatkan warna, atau daftar elemen yang perlu ditangani oleh komponen. Anda dapat menggunakan fungsi terikat seperti yang disarankan beberapa jawaban, atau Anda dapat menggunakan antarmuka (itu selalu merupakan preferensi saya).

Contoh

Misalkan Anda memiliki komponen generik yang beroperasi di atas daftar elemen {id, name} yang ingin Anda gunakan dengan semua tabel database Anda yang memiliki bidang ini. Komponen ini harus:

  • mengambil serangkaian elemen (halaman) dan menunjukkannya dalam daftar
  • bolehkan hapus elemen
  • informasikan bahwa suatu elemen telah diklik, sehingga orang tua dapat mengambil beberapa tindakan.
  • bolehkan mengambil kembali elemen halaman berikutnya.

Komponen Anak

Dengan menggunakan penjilidan normal kita membutuhkan 1 @Input()dan 3 @Output()parameter (tetapi tanpa umpan balik dari orang tua). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, tetapi membuat antarmuka, kita hanya perlu satu @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); 
    }
}

Komponen Induk

Sekarang kita dapat menggunakan komponen daftar di induk.

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

Perhatikan bahwa <list-ctrl>menerimathis (komponen induk) sebagai objek panggilan balik. Satu keuntungan tambahan adalah tidak diperlukan untuk mengirimkan instance induk, itu bisa berupa layanan atau objek apa pun yang mengimplementasikan antarmuka jika use case Anda mengizinkannya.

Contoh lengkap ada di stackblitz ini .

WPomier
sumber
-3

Jawaban saat ini dapat disederhanakan untuk ...

@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; 
  ...
}
Biru
sumber
jadi tidak perlu untuk mengikat secara eksplisit?
Michail Michailidis
3
Tanpa .bind(this)maka thisdalam callback akan windowyang mungkin tidak peduli tergantung pada kasus penggunaan Anda. Namun jika Anda memiliki thispanggilan balik sama sekali, maka .bind(this)perlu. Jika Anda tidak melakukannya, maka versi yang disederhanakan ini adalah caranya.
SnareChops
3
Saya sarankan untuk selalu mengikat callback dengan komponen, karena pada akhirnya Anda akan menggunakan thisfungsi callback di dalam. Itu hanya rawan kesalahan.
Alexandre Junges
Itu adalah contoh antipattern Angular 2.
Serginho
Tidak harus anti-pola. Ada kasus di mana Anda menginginkan ini. Bukan hal yang tidak biasa untuk memberitahu komponen BAGAIMANA melakukan sesuatu yang bukan tentang tampilan. Itu masuk akal dan saya tidak melihat mengapa jawaban ini mendapatkan begitu banyak kebencian.
Lazar Ljubenović