Hàm gọi lại qua góc tới thành phần con là @Input tương tự như cách AngularJS


227

AngularJS có các tham số & trong đó bạn có thể chuyển một cuộc gọi lại cho một lệnh (ví dụ: cách gọi lại của AngularJS . Có thể chuyển một cuộc gọi lại như là một @InputThành phần Angular (giống như bên dưới) không? AngularJS nào?

@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
đối với những độc giả tương lai, @Inputcách gợi ý đã làm cho mã spagetti của tôi và không dễ duy trì .. @Outputđó là một cách tự nhiên hơn nhiều để làm những gì tôi muốn. Kết quả là tôi đã thay đổi câu trả lời được chấp nhận
Michail Michailidis

Câu hỏi @IanS là về cách một cái gì đó được thực hiện trong Angular tương tự như AngularJS? Tại sao tiêu đề sai lệch?
Michail Michailidis

Angular rất khác với AngularJS. Angular 2+ chỉ là Angular.
Ian S

1
Đã sửa tiêu đề của bạn;)
Ian S

1
@IanS Cảm ơn! bây giờ câu hỏi là về angularJs - mặc dù với thẻ bạn đã thêm.
Michail Michailidis

Câu trả lời:


296

Tôi nghĩ rằng đó là một giải pháp tồi. Nếu bạn muốn truyền Hàm vào thành phần @Input(), @Output()trang trí là thứ bạn đang tìm kiếm.

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
Nói chính xác là bạn không truyền chức năng mà thay vào đó là kết nối một trình lắng nghe sự kiện của người nghe với đầu ra. Hữu ích để hiểu tại sao nó hoạt động.
Jens

13
Đây là một phương pháp tuyệt vời, nhưng tôi đã để lại rất nhiều câu hỏi sau khi đọc câu trả lời này. Tôi đã hy vọng nó sẽ có chiều sâu hơn hoặc có một liên kết được cung cấp mô tả @OutputEventEmitter. Vì vậy, đây là tài liệu Angular cho @Output cho những người quan tâm.
WebWanderer

9
Điều này là tốt cho ràng buộc một chiều. Bạn có thể kết nối với sự kiện của trẻ em. Nhưng bạn không thể truyền chức năng gọi lại cho trẻ và để nó phân tích giá trị trả về của cuộc gọi lại. Câu trả lời dưới đây cho phép điều đó.
rook

3
Tôi hy vọng sẽ có nhiều lời giải thích hơn về lý do tại sao thích cách này hơn cách khác thay vì có "Tôi nghĩ đó là một giải pháp tồi."
Fidan Hakaj

6
Có lẽ tốt cho 80% các trường hợp, nhưng không phải khi một thành phần con muốn trực quan hóa có điều kiện về việc có gọi lại hay không.
John Freeman

115

CẬP NHẬT

Câu trả lời này đã được gửi khi Angular 2 vẫn ở trạng thái alpha và nhiều tính năng không khả dụng / không có giấy tờ. Trong khi cách dưới đây vẫn sẽ hoạt động, phương pháp này hiện đã hoàn toàn lỗi thời. Tôi mạnh mẽ đề nghị trả lời chấp nhận so với bên dưới.

Câu trả lời gốc

Có trong thực tế nó là, tuy nhiên bạn sẽ muốn chắc chắn rằng nó được đặt trong phạm vi chính xác. Đối với điều này, tôi đã sử dụng một tài sản để đảm bảo điều đó thiscó nghĩa là những gì tôi muốn nó.

@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
Điều này đã làm việc! Cảm ơn! Tôi ước tài liệu có ở đâu đó :)
Michail Michailidis

1
Bạn có thể sử dụng một phương thức tĩnh nếu bạn muốn, nhưng sau đó bạn sẽ không có quyền truy cập vào bất kỳ thành viên thể hiện nào của thành phần. Vì vậy, có lẽ không phải trường hợp sử dụng của bạn. Nhưng vâng, bạn cũng cần phải vượt qua điều đó từParent -> Child
SnareChops 11/2/2016

3
Câu trả lời chính xác! Tôi thường không đổi tên hàm khi ràng buộc. trong ngOnInittôi sẽ chỉ sử dụng: this.theCallback = this.theCallback.bind(this)và sau đó bạn có thể vượt qua theCallbackthay vì theBoundCallback.
Zack

1
@MichailMichailidis Vâng, tôi đồng ý với giải pháp của bạn và đã cập nhật câu trả lời của tôi với một lưu ý để dẫn mọi người đến cách tốt hơn. Cảm ơn đã để mắt đến cái này
SnareChops

7
@Output và EventEuctor vẫn ổn cho ràng buộc một chiều. Bạn có thể kết nối với sự kiện của trẻ nhưng bạn không thể truyền chức năng gọi lại cho trẻ và để nó phân tích giá trị trả về của cuộc gọi lại. Câu trả lời này cho phép điều đó.
rook

31

Một thay thế cho câu trả lời SnareChops đã đưa ra.

Bạn có thể sử dụng .bind (cái này) trong mẫu của bạn để có hiệu ứng tương tự. Nó có thể không sạch sẽ nhưng nó tiết kiệm một vài dòng. Tôi hiện đang ở góc 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
như những người khác đã nhận xét ràng buộc (cái này) trong mẫu không được ghi lại để nó có thể bị phản đối / không được hỗ trợ trong tương lai. Thêm một lần nữa @Inputkhiến mã trở thành spaghetti và sử dụng @Outputkết quả trong một quy trình tự nhiên / không bị rối hơn
Michail Michailidis

1
Khi bạn đặt bind () trong mẫu, Angular sẽ đánh giá lại biểu thức này ở mỗi lần phát hiện thay đổi. Giải pháp khác - thực hiện liên kết bên ngoài mẫu - ít súc tích hơn, nhưng nó không có vấn đề này.
Chris

Câu hỏi: khi làm .bind (này), bạn đang ràng buộc phương thức theCallBack với con hoặc cha mẹ? Tôi nghĩ đó là với đứa trẻ. Nhưng vấn đề là, khi liên kết được gọi, luôn luôn là đứa trẻ gọi nó, vì vậy liên kết này dường như không cần thiết nếu tôi đúng.
ChrisZ

Nó liên kết với thành phần cha. Lý do điều này được thực hiện là vì khiCallBack () được gọi, nó có thể sẽ muốn làm một cái gì đó bên trong chính nó và nếu "đây" không phải là thành phần cha mẹ thì nó sẽ nằm ngoài ngữ cảnh và do đó không thể tiếp cận các phương thức và biến của chính nó nữa không.
Tối đa

29

Trong một số trường hợp, bạn có thể cần logic nghiệp vụ được thực hiện bởi một thành phần cha. Trong ví dụ dưới đây, chúng ta có một thành phần con biểu hiện hàng của bảng tùy thuộc vào logic được cung cấp bởi thành phần cha mẹ:

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

Vì vậy, tôi muốn chứng minh 2 điều ở đây:

  1. Hàm mũi tên béo (=>) thay vì .bind (này) để giữ đúng ngữ cảnh;
  2. Khai báo kiểu an toàn của hàm gọi lại trong thành phần con.

1
Giải thích tuyệt vời cho việc sử dụng mũi tên béo để thay thế việc sử dụng.bind(this)
TYMG

6
Mẹo sử dụng: Đảm bảo đặt [getRowColor]="getColor"và không [getRowColor]="getColor()";-)
Simon_Weaver

Đẹp. Điều này thật đúng với gì mà tôi đã tìm kiếm. Đơn giản & hiệu quả.
BrainSlugs83

7

Ví dụ, tôi đang sử dụng cửa sổ phương thức đăng nhập, trong đó cửa sổ phương thức là cha mẹ, hình thức đăng nhập là con và nút đăng nhập gọi lại chức năng đóng của cha mẹ phương thức.

Phương thức cha chứa hàm để đóng phương thức. Cha mẹ này chuyển chức năng đóng cho thành phần con đăng nhập.

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

Sau khi thành phần đăng nhập con gửi biểu mẫu đăng nhập, nó sẽ đóng chế độ cha mẹ bằng cách sử dụng chức năng gọi lại của cha mẹ

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

Một thay thế cho câu trả lời Max Fahl đã đưa ra.

Bạn có thể định nghĩa hàm gọi lại là hàm mũi tên trong thành phần cha mẹ để bạn không cần phải liên kết nó.

@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

Truyền phương thức với đối số, sử dụng .bind bên trong mẫu

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

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

  @Input()
  public action: Function; 

  ...
}

Không phải câu trả lời của bạn về cơ bản giống như thế này: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis

trả lời bình luận này stackoverflow.com/questions35328652/iêu
Shogg


0

Một lựa chọn khác.

OP đã hỏi một cách để sử dụng một cuộc gọi lại. Trong trường hợp này, anh ấy đã đề cập cụ thể đến một chức năng xử lý một sự kiện (trong ví dụ của anh ấy: một sự kiện nhấp chuột), sẽ được coi là câu trả lời được chấp nhận từ @serginho gợi ý: với @OutputEventEmitter .

Tuy nhiên, có một sự khác biệt giữa một cuộc gọi lại và một sự kiện: Với một cuộc gọi lại, thành phần con của bạn có thể lấy một số phản hồi hoặc thông tin từ cha mẹ, nhưng một sự kiện chỉ có thể thông báo rằng có điều gì đó đã xảy ra mà không mong đợi bất kỳ phản hồi nào.

Có những trường hợp sử dụng trong đó một phản hồi là cần thiết, ví dụ. có được một màu, hoặc một danh sách các yếu tố mà thành phần cần xử lý. Bạn có thể sử dụng các hàm bị ràng buộc như một số câu trả lời đã đề xuất hoặc bạn có thể sử dụng các giao diện (đó luôn là sở thích của tôi).

Thí dụ

Giả sử bạn có một thành phần chung hoạt động trên danh sách các phần tử {id, name} mà bạn muốn sử dụng với tất cả các bảng cơ sở dữ liệu của bạn có các trường này. Thành phần này nên:

  • lấy một loạt các yếu tố (trang) và hiển thị chúng trong một danh sách
  • cho phép loại bỏ một yếu tố
  • thông báo rằng một phần tử đã được nhấp, để cha mẹ có thể thực hiện một số hành động.
  • cho phép lấy trang tiếp theo của các phần tử.

Thành phần con

Sử dụng liên kết thông thường, chúng ta sẽ cần 1 @Input()và 3 @Output()tham số (nhưng không có bất kỳ phản hồi nào từ cha mẹ). Ví dụ. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, nhưng tạo một giao diện, chúng ta sẽ chỉ cần một @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); 
    }
}

Thành phần phụ huynh

Bây giờ chúng ta có thể sử dụng thành phần danh sách trong cha mẹ.

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

Lưu ý rằng <list-ctrl>nhậnthis (thành phần cha) là đối tượng gọi lại. Một lợi thế nữa là không bắt buộc phải gửi cá thể cha, nó có thể là một dịch vụ hoặc bất kỳ đối tượng nào thực hiện giao diện nếu trường hợp sử dụng của bạn cho phép.

Ví dụ đầy đủ là trên stackblitz này .


-3

Câu trả lời hiện tại có thể được đơn giản hóa để ...

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

Vì vậy, không cần phải ràng buộc rõ ràng?
Michail Michailidis

3
Nếu không có .bind(this)thì thisbên trong của cuộc gọi lại sẽ windowcó thể không quan trọng tùy thuộc vào trường hợp sử dụng của bạn. Tuy nhiên nếu bạn có thistrong cuộc gọi lại, thì điều đó .bind(this)là cần thiết. Nếu bạn không thì phiên bản đơn giản hóa này là cách để đi.
SnareChops

3
Tôi khuyên bạn luôn luôn liên kết gọi lại với thành phần, vì cuối cùng bạn sẽ sử dụng thisbên trong chức năng gọi lại. Nó chỉ dễ bị lỗi.
Alexandre Junges

Đó là một ví dụ về một antipotype 2 góc.
Serginho

Nó không phải là một mô hình chống. Có những trường hợp bạn muốn chính xác điều này. Không có gì lạ khi muốn nói với thành phần CÁCH làm điều gì đó không liên quan đến chế độ xem. Nó có ý nghĩa và tôi không thấy lý do tại sao câu trả lời này lại nhận được nhiều sự ghét bỏ như vậy.
Lazar Ljubenović
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.