Angular将回调函数作为@Input传递给子组件,类似于AngularJS的方式


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建议的方式使我的代码变得混乱,并且不易维护@Output。结果,我更改了接受的答案
Michail Michailidis

@IanS问题是关于如何在Angular中完成类似于AngularJS的操作?为什么标题有误导性?
Michail Michailidis

Angular与AngularJS有很大的不同。Angular 2+就是Angular。
伊恩S

1
固定了标题;)
Ian S

1
@IanS谢谢!现在的问题也是关于angularJs的-尽管添加了标签。
Michail Michailidis

Answers:


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
准确地说,您不是在传递函数,而是将侦听器事件侦听器连接到输出。有助于理解其工作原理。
詹斯

13
这是一个很好的方法,但是阅读此答案后,我遇到了很多问题。我希望它会更深入,或者提供一个描述@Output和链接EventEmitter。因此,这里是针对@OutputAngular文档
WebWanderer

9
这对于单向装订很好。您可以联系孩子的活动。但是您不能将回调函数传递给子级,而让它分析回调的返回值。下面的答案允许。
菜鸟

3
我希望对为什么偏爱一种方式而不是“我认为这是一个糟糕的解决方案”有更多的解释。
Fidan Hakaj

6
可能对80%的情况有用,但在子组件希望可视化取决于回调是否存在的情况下则不然。
约翰·弗里曼

115

更新

当Angular 2仍处于Alpha状态且许多功能不可用/未记录时,提交了此答案。尽管下面的方法仍然可以使用,但是现在该方法已经完全过时了。我强烈建议您接受以下答案。

原始答案

是的,实际上是,但是您将需要确保其作用域正确。为此,我使用了一个属性来确保这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
这工作了!谢谢!我希望文档在某处:)
Michail Michailidis

1
如果愿意,可以使用静态方法,但是您将无权访问该组件的任何实例成员。所以可能不是您的用例。但是,是的,您也需要从Parent -> Child
SnareChops

3
好答案!我通常在绑定时不重命名该函数。在ngOnInit我只想用:this.theCallback = this.theCallback.bind(this)然后你就可以传递下去theCallback,而不是theBoundCallback
扎克

1
@MichailMichailidis是的,我同意您的解决方案,并在注释中更新了我的答案,以引导人们找到更好的方法。感谢您关注这一点。
SnareChops

7
@Output和EventEmitter适用于单向绑定。您可以连接到孩子的事件,但不能将回调函数传递给孩子,让它分析回调的返回值。这个答案允许。
菜鸟

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在更自然/更
整洁的

1
当您将bind()放在模板中时,Angular会在每次更改检测时重新评估此表达式。另一种解决方案-在模板外部进行绑定-不够简洁,但是它没有这个问题。
克里斯(Chris)

问题:在执行.bind(this)时,您是将方法theCallBack与孩子还是父母进行绑定?我认为是和孩子在一起。但问题是,当绑定被调用时,始终是孩子在调用它,因此,如果我是对的,则似乎不需要此绑定。
ChrisZ

它与父组件绑定。这样做的原因是,在调用CallCallBack()时,它可能想在其内部做某事,并且如果“ 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);
  }
}

因此,我想在这里演示两件事:

  1. 粗箭头(=>)代替.bind(this)函数来保持正确的上下文;
  2. 子组件中回调函数的Typesafe声明。

1
关于使用.bind(this)

6
使用技巧:确保放置[getRowColor]="getColor"而不是[getRowColor]="getColor()";-)
Simon_Weaver

真好 这正是我想要的。简单有效。
BrainSlugs83

7

例如,我使用的是登录模式窗口,其中模式窗口是父窗口,登录表单是子窗口,而登录按钮则回调到模式父窗口的关闭功能。

父模态包含关闭模态的功能。此父级将close函数传递给登录子级组件。

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

Max Fahl给出的答案的替代方案。

您可以在父组件中将回调函数定义为箭头函数,这样就无需绑定它。

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



0

另一种选择。

OP询问了使用回调的方法。在这种情况下,他专门指的是处理事件的函数(在他的示例中为click事件),应将其视为@serginho建议的可接受答案:@OutputEventEmitter

但是,回调和事件之间是有区别的:通过回调,您的子组件可以从父级检索一些反馈或信息,但是一个事件只能通知某些事情发生了,而无需任何反馈。

在某些情况下,需要反馈,例如。获取颜色或组件需要处理的元素列表。您可以按照一些答案的建议使用绑定函数,也可以使用接口(这始终是我的偏好)。

假设您有一个通用组件,该组件在要与所有具有这些字段的数据库表一起使用的元素{id,name}列表上进行操作。该组件应:

  • 检索一系列元素(页面)并将其显示在列表中
  • 允许删除一个元素
  • 告知已单击某个元素,因此父级可以采取一些措施。
  • 允许检索元素的下一页。

子组件

使用普通绑定,我们将需要1 @Input()和3个@Output()参数(但没有父级的任何反馈)。例如 <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>,但是创建一个接口,我们只需要一个接口@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>receives this(父组件)作为回调对象。另一个优点是不需要发送父实例,它可以是服务或实现接口的任何对象(如果您的用例允许)。

完整的示例在此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; 
  ...
}

因此,无需显式绑定吗?
Michail Michailidis

3
如果没有,.bind(this)this回调的内部将window可能无关紧要,具体取决于您的用例。但是,如果您完全this在回调中,则.bind(this)很有必要。如果您不这样做,那么此简化版本是可行的方法。
SnareChops

3
我建议始终将回调与组件绑定,因为最终您将this在回调函数内部使用。只是容易出错。
Alexandre Junges'8

那是Angular 2反模式的一个例子。
塞尔吉尼奥(Serginho)

它不必是反模式。在某些情况下,您确实需要这样做。想要告诉组件如何做与视图无关的事情并不少见。这是有道理的,我不明白为什么这个答案越来越令人讨厌。
LazarLjubenović18年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.