ฟังก์ชั่นการโทรกลับ 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วิธีการอ่านในอนาคตที่แนะนำทำให้รหัสของฉัน spagetti และไม่ง่ายต่อการรักษา .. @Outputs เป็นวิธีที่เป็นธรรมชาติมากขึ้นในการทำสิ่งที่ฉันต้องการ เป็นผลให้ฉันเปลี่ยนคำตอบที่ยอมรับ
Michail Michailidis

@IanS คำถามเกี่ยวกับสิ่งที่ทำในเชิงมุมคล้ายกับ AngularJS? ทำไมชื่อเรื่องจึงทำให้เข้าใจผิด?
Michail Michailidis

Angular นั้นแตกต่างจาก AngularJS มาก Angular 2+ เป็นเพียง Angular
Ian S

1
แก้ไขชื่อของคุณ;)
Ian S

1
@IanS ขอบคุณ! ตอนนี้คำถามเกี่ยวกับ angularJs ด้วย - ด้วยแท็กที่คุณเพิ่ม
Michail Michailidis

คำตอบ:


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
เพื่อความแม่นยำคุณจะไม่ผ่านฟังก์ชั่น แต่ให้ลองฟัง listener event listener ไปที่ output มีประโยชน์สำหรับการทำความเข้าใจว่าทำไมจึงทำงาน
Jens

13
นี่เป็นวิธีที่ดี แต่ฉันก็เหลือคำถามมากมายหลังจากอ่านคำตอบนี้ ผมก็หวังว่ามันจะมีมากขึ้นในเชิงลึกหรือมีลิงก์ที่ให้อธิบายและ@Output EventEmitterดังนั้นนี่คือเอกสารเชิงมุมสำหรับ @Outputสำหรับผู้ที่สนใจ
WebWanderer

9
นี่เป็นเรื่องปกติสำหรับการผูกทางเดียว คุณสามารถเชื่อมต่อกับกิจกรรมของเด็ก แต่คุณไม่สามารถส่งผ่านฟังก์ชันการโทรกลับไปยังเด็กและให้มันวิเคราะห์ค่าส่งคืนของการโทรกลับ คำตอบด้านล่างช่วยให้ที่
rook

3
ฉันคาดหวังว่าจะมีคำอธิบายเพิ่มเติมเกี่ยวกับสาเหตุที่ชอบมากกว่าวิธีอื่นแทนที่จะมี "ฉันคิดว่านั่นเป็นวิธีที่ไม่ดี"
Fidan Hakaj

6
น่าจะดีสำหรับ 80% ของกรณี แต่ไม่ใช่เมื่อองค์ประกอบลูกต้องการให้เห็นภาพตามเงื่อนไขว่ามีการโทรกลับหรือไม่
John Freeman

115

UPDATE

คำตอบนี้ถูกส่งเมื่อ 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
สิ่งนี้ได้ผล! ขอบคุณ! ฉันหวังว่าเอกสารมีที่ใดที่หนึ่ง :)
Michail Michailidis

1
คุณสามารถใช้วิธีการคงที่หากคุณต้องการ แต่จากนั้นคุณจะไม่สามารถเข้าถึงสมาชิกอินสแตนซ์ขององค์ประกอบใด ๆ ดังนั้นอาจไม่ใช่กรณีการใช้งานของคุณ แต่ใช่คุณจะต้องผ่านสิ่งนั้นด้วยParent -> Child
SnareChops

3
คำตอบที่ดี! ฉันมักจะไม่เปลี่ยนชื่อฟังก์ชั่นเมื่อมีผลผูกพัน ในngOnInitฉันจะใช้: this.theCallback = this.theCallback.bind(this)แล้วคุณสามารถผ่านพร้อมแทนtheCallback theBoundCallback
แซค

1
@MichailMichailidis ใช่ฉันเห็นด้วยกับวิธีการแก้ปัญหาของคุณและได้อัปเดตคำตอบของฉันพร้อมหมายเหตุเพื่อนำผู้คนไปสู่วิธีที่ดีกว่า ขอบคุณที่คอยจับตาดูสิ่งนี้
SnareChops

7
@ Output และ EventEmitter นั้นใช้ได้สำหรับการเชื่อมโยงทางเดียว คุณสามารถเชื่อมต่อกับกิจกรรมของเด็ก แต่คุณไม่สามารถส่งผ่านฟังก์ชันการโทรกลับไปยังเด็กและให้มันวิเคราะห์ค่าส่งกลับของการโทรกลับ คำตอบนี้จะช่วยให้
rook

31

อีกทางเลือกหนึ่งสำหรับคำตอบ SnareChops

คุณสามารถใช้. ผูก (สิ่งนี้) ในแม่แบบของคุณเพื่อให้มีผลเหมือนกัน มันอาจจะไม่สะอาดเหมือน แต่มันช่วยประหยัดสองบรรทัด ตอนนี้ฉันอยู่ที่เชิงมุม 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
ในขณะที่คนอื่น ๆ ได้แสดงความคิดเห็นผูก (นี้) ในแม่แบบที่ไม่มีเอกสารดังนั้นมันอาจกลายเป็นเลิก / ไม่ได้รับการสนับสนุนในอนาคต บวกอีกครั้ง@Inputทำให้รหัสกลายเป็นสปาเก็ตตี้และใช้@Outputผลลัพธ์ในกระบวนการที่เป็นธรรมชาติ / ไม่ถูกพันกัน
Michail Michailidis

1
เมื่อคุณวาง bind () ในเทมเพลต Angular จะประเมินนิพจน์นี้อีกครั้งที่การตรวจจับการเปลี่ยนแปลงทุกครั้ง โซลูชันอื่น ๆ - การทำการผูกนอกเทมเพลต - กระชับน้อยลง แต่ไม่มีปัญหานี้
Chris

คำถาม: เมื่อทำ. ผูก (นี้) คุณมีวิธีการผูกมัดโทรกลับกับเด็กหรือผู้ปกครอง? ฉันคิดว่ามันกับเด็ก แต่สิ่งที่เกิดขึ้นคือเมื่อมีการเรียกการผูกมันมักจะเรียกว่าเด็กเสมอดังนั้นการผูกนี้จึงไม่จำเป็นถ้าฉันถูกต้อง
ChrisZ

มันผูกกับองค์ประกอบหลัก เหตุผลที่ทำเช่นนี้คือเมื่อเรียก theCallBack () มันอาจจะต้องการทำอะไรบางอย่างภายในตัวเองและถ้า "นี่" ไม่ใช่องค์ประกอบหลักมันจะไม่อยู่ในบริบทดังนั้นจึงไม่สามารถเข้าถึงวิธีการและตัวแปรของตัวเองได้ อีกต่อไป
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. ลูกศรอ้วน (=>) ฟังก์ชั่นแทน. ผูก (นี่) เพื่อเก็บบริบทที่ถูกต้อง;
  2. ประกาศ typesafe ของฟังก์ชันการเรียกกลับในองค์ประกอบลูก

1
คำอธิบายที่ดีสำหรับการใช้ลูกศรไขมันเพื่อแทนที่การใช้.bind(this)
TYMG

6
เคล็ดลับการใช้งาน: ตรวจสอบให้แน่ใจว่าได้ใส่[getRowColor]="getColor"และไม่[getRowColor]="getColor()";-)
Simon_Weaver

ดี นี่คือสิ่งที่ฉันกำลังมองหา ง่ายและมีประสิทธิภาพ
BrainSlugs83

7

ตัวอย่างเช่นฉันใช้หน้าต่าง modal เข้าสู่ระบบที่หน้าต่าง modal เป็นผู้ปกครองรูปแบบการเข้าสู่ระบบเป็นเด็กและปุ่มเข้าสู่ระบบโทรกลับไปที่ฟังก์ชั่นใกล้ชิดของผู้ปกครอง modal

modal parent มีฟังก์ชันเพื่อปิด modal พาเรนต์นี้ส่งผ่านฟังก์ชันปิดไปยังคอมโพเนนต์ชายด์ของล็อกอิน

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

หลังจากที่องค์ประกอบการเข้าสู่ระบบเด็กส่งแบบฟอร์มการเข้าสู่ระบบมันจะปิด modal ปกครองโดยใช้ฟังก์ชั่นการโทรกลับของผู้ปกครอง

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

วิธีการส่งผ่านที่มีอาร์กิวเมนต์ใช้. ผูกภายในเทมเพลต

@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

ใช้รูปแบบที่สังเกตได้ คุณสามารถใส่ค่าที่สังเกตได้ (ไม่ใช่หัวเรื่อง) ลงในพารามิเตอร์อินพุตและจัดการจากส่วนประกอบหลัก คุณไม่ต้องการฟังก์ชั่นโทรกลับ

ดูตัวอย่าง: https://stackoverflow.com/a/49662611/4604351


คุณช่วยอธิบายตัวอย่างด้วยตัวอย่างการทำงานได้ไหม
Michail Michailidis

0

ทางเลือกอื่น

OP ขอวิธีการใช้โทรกลับ ในกรณีนี้เขาหมายเฉพาะเพื่อฟังก์ชั่นที่ประมวลผลเหตุการณ์ (ในตัวอย่างของเขาเหตุการณ์คลิก) ซึ่งจะต้องได้รับการปฏิบัติเป็นคำตอบที่ได้รับการยอมรับจาก @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>ได้รับ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

นั่นคือตัวอย่างของแองกูลาร์ 2 Angular
Serginho

มันไม่จำเป็นต้องเป็นแบบป้องกัน มีหลายกรณีที่คุณต้องการสิ่งนี้ ไม่ใช่เรื่องแปลกที่ต้องการบอกองค์ประกอบวิธีทำสิ่งที่ไม่เกี่ยวกับมุมมอง มันสมเหตุสมผลและฉันไม่เห็นว่าทำไมคำตอบนี้ถึงได้เกลียดมาก
Lazar Ljubenović
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.