AngularJSの方法と同様に、@ Inputとして子コンポーネントにAngularパスコールバック関数を渡します


227

AngularJSには、コールバックをディレクティブに渡すことができる&パラメータがあります(たとえば、AngularJSのコールバック方法です。コールバックを@InputAngular Component(以下のようなもの)のとして渡すことは可能ですか?そうでない場合、最も近いものは何ですか? AngularJSはそうですか?

@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
将来の読者の@Inputために、提案された方法は私のコードをspagettiにし、維持することを容易にしませんでした@Output。その結果、私は受け入れられた答えを変更しました
ミハイル・ミシャイリディス'20 / 11/16

@IanS質問は、AngularJSと同様にAngularでどのように行われるかについてです タイトルが誤解を招くのはなぜですか?
Michail Michailidis

AngularはAngularJSとは大きく異なります。Angular 2+はまさにAngularです。
Ian S

1
タイトルを修正しました;)
Ian S

1
@IanSありがとう!さて、問題はangularJsについてもです-あなたが追加したタグで。
ミハイルミシャイリディス

回答:


296

それは悪い解決策だと思います。を使用して関数をコンポーネントに渡したい場合は@Input()@Output()デコレータが必要です。

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
正確には、関数を渡すのではなく、リスナーイベントリスナーを出力にフックします。それが機能する理由を理解するのに役立ちます。
Jens

13
これは素晴らしい方法ですが、この回答を読んだ後、多くの質問が残されました。私はそれがより詳細になるか、説明@Outputとリンクが提供されることを望んでいましたEventEmitter。それで、興味のある人のための@OutputAngularドキュメントです
WebWanderer 2017年

9
これは一方向のバインディングには問題ありません。あなたは子供のイベントにフックアップすることができます。ただし、コールバック関数を子に渡して、コールバックの戻り値を分析させることはできません。以下の答えはそれを可能にします。
2017

3
「それは悪い解決策だと思います」ではなく、ある方法を別の方法よりも好む理由について、もっと詳しい説明があると思います。
Fidan Hakaj 2017年

6
おそらく80%の場合に適していますが、コールバックが存在するかどうかを条件として子コンポーネントが視覚化を必要とする場合はそうではありません。
John Freeman、

115

更新

この回答は、Angular 2がまだアルファ版であり、多くの機能が利用できない/文書化されていないときに送信されました。以下はまだ機能しますが、この方法は完全に古くなっています。以下よりも、受け入れられた回答を強くお勧めします。

元の回答

はい、実際にはそうですが、スコープが正しく設定されていることを確認する必要があります。このため、私はプロパティを使用して、thisそれが私が望んでいることを意味することを確認しました。

@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
これはうまくいった!ありがとう!ドキュメンテーションにどこかにそれがあったらいいのに:)
ミハイルミシャイリディス

1
必要に応じて静的メソッドを使用できますが、その場合、コンポーネントのインスタンスメンバーにはアクセスできません。おそらくあなたのユースケースではないでしょう。しかし、はい、あなたもそれを渡す必要がありますParent -> Child
SnareChops

3
正解です。バインドするときは通常、関数の名前を変更しません。でngOnInit、私はちょうど使用しますthis.theCallback = this.theCallback.bind(this)と、あなたは一緒に渡すことができますtheCallback代わりにtheBoundCallback
ザック2016年

1
@MichailMichailidisはい、私はあなたの解決策に同意し、人々をより良い方法に導くために私の回答をメモで更新しました。これを監視してくれてありがとう。
SnareChops 2016年

7
@OutputとEventEmitterは、一方向バインディングで問題ありません。子のイベントに接続できますが、コールバック関数を子に渡して、コールバックの戻り値を分析させることはできません。この答えはそれを可能にします。
2017

31

SnareChopsが与えた答えの代替案。

テンプレートで.bind(this)を使用して、同じ効果を得ることができます。それほどきれいではないかもしれませんが、数行を節約します。私は現在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; 
  ...
}

2
他の人がテンプレートのbind(this)についてコメントしているので、ドキュメント化されていないため、将来は非推奨/サポートされなくなる可能性があります。さらに@Input、コードがスパゲッティになり、@Output結果がより自然で複雑なプロセスになる
Michail Michailidis 2017

1
テンプレートにbind()を配置すると、Angularはすべての変更検出でこの式を再評価します。その他の解決策-テンプレートの外部でバインドを行う-は簡潔ですが、この問題はありません。
クリス

質問:.bind(this)を実行するとき、メソッドtheCallBackを子または親にバインドしていますか?子供と一緒だと思います。しかし、問題は、バインドが呼び出されているとき、それは常に子を呼び出しているため、私が正しければ、このバインドは必要ないようです。
ChrisZ

親コンポーネントとバインドします。これが行われる理由は、theCallBack()が呼び出されているときに、おそらくそれ自体の内部で何かを実行する必要があり、「this」が親コンポーネントでない場合、コンテキストから外れ、独自のメソッドと変数に到達できないためです。もう。
Max Fahl

29

場合によっては、ビジネスロジックを親コンポーネントで実行する必要があります。以下の例では、親コンポーネントによって提供されるロジックに応じてテーブル行をレンダリングする子コンポーネントがあります。

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

そこで、私はここで2つのことを実証したいと思いました。

  1. 適切なコンテキストを保持するために、.bind(this)の代わりに太い矢印(=>)機能。
  2. 子コンポーネントのコールバック関数のタイプセーフな宣言。

1
ファットアローを使用して代わりの使用を説明する素晴らしい説明.bind(this)
TYMG

6
使い方のヒント:[getRowColor]="getColor"しないでください[getRowColor]="getColor()";-)
Simon_Weaver

いいね。これはまさに私が探していたものです。シンプルで効果的。
BrainSlugs83

7

例として、ログインモーダルウィンドウを使用しています。モーダルウィンドウは親であり、ログインフォームは子であり、ログインボタンはモーダル親のclose関数を呼び出します。

親モーダルには、モーダルを閉じる関数が含まれています。この親は、close関数を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();
  }
}

子ログインコンポーネントがログインフォームを送信した後、親のコールバック関数を使用して親モーダルを閉じます

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

マックス・ファールが与えた答えの代替案。

バインドする必要がないように、親コンポーネントでコールバック関数を矢印関数として定義できます。

@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

テンプレート内で.bindを使用して、引数を指定してメソッドを渡す

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

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

  @Input()
  public action: Function; 

  ...
}

あなたの答えは基本的にこれと同じではありません:stackoverflow.com/a/42131227/986160
Michail Michailidis

このコメントに答えるstackoverflow.com/questions/35328652/...
Shogg


0

別の選択肢。

OPは、コールバックを使用する方法を尋ねました。この場合、彼は特にイベントを処理する関数(彼の例ではクリックイベント)を参照していました。これは、@ serginhoから受け入れられた回答として扱われます:with @OutputおよびEventEmitter

ただし、コールバックとイベントには違いがあります。コールバックを使用すると、子コンポーネントは親からフィードバックや情報を取得できますが、イベントはフィードバックを期待せずに何かが発生したことを通知するだけです。

フィードバックが必要なユースケースがあります。コンポーネントが処理する必要がある色または要素のリストを取得します。いくつかの回答が示唆しているように、バインドされた関数を使用することも、インターフェイスを使用することもできます(これは常に私の好みです)。

これらのフィールドを持つすべてのデータベーステーブルで使用する要素{id、name}のリストを操作する汎用コンポーネントがあるとします。このコンポーネントは:

  • 要素の範囲(ページ)を取得し、リストに表示する
  • 要素の削除を許可する
  • 要素がクリックされたことを通知し、親がいくつかのアクションを実行できるようにします。
  • 要素の次のページを取得できます。

子コンポーネント

通常のバインディングを使用するには、1 @Input()つと3つの@Output()パラメーターが必要です(ただし、親からのフィードバックはありません)。例 <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>、しかしインターフェースを作成するのに必要なものは1つだけ@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); 
    }
}

親コンポーネント

これで、リストコンポーネントを親で使用できます。

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

<list-ctrl>受け取るthis(親コンポーネント)コールバックオブジェクトとして注意してください。もう1つの利点は、親インスタンスを送信する必要がないことです。ユースケースで許可されている場合は、サービスまたはインターフェイスを実装するオブジェクトにすることができます。

完全な例は、このstackblitzにあります。


-3

現在の答えは、次のように簡略化できます...

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

明示的にバインドする必要はありませんか?
ミハイル

3
そうしないと、コールバック.bind(this)this内部は、windowユースケースによっては問題にならない場合があります。ただしthis、コールバック.bind(this)がある場合は、必要です。そうしない場合は、この簡略化されたバージョンが適しています。
SnareChops 2016年

3
最終的thisにはコールバック関数内で使用するため、常にコールバックをコンポーネントにバインドすることをお勧めします。エラーが発生しやすいだけです。
Alexandre Junges 16

これは、Angular 2アンチパターンの例です。
Serginho 2016年

アンチパターンである必要はありません。これが正確に必要な場合があります。コンポーネントのHOWに、ビューとは関係のないことを実行するように指示することは、それほど珍しいことではありません。それは理にかなっていて、なぜこの答えがそれほど多くの憎悪を抱いているのかわかりません。
LazarLjubenović18年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.