Кутова передача функції зворотного дзвінка до дочірнього компонента як @ Input, аналогічна AngularJS


227

AngularJS має & параметри, де ви можете передати зворотний виклик директиві (наприклад, спосіб зворотного виклику AngularJS . Чи можна передавати зворотний виклик як @Inputдля кутового компонента (щось подібне нижче)? Якщо ні, що було б найближчим до того, що 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способом запропоновано зробити мій код спагетті і не простий у обслуговуванні .. @Outputs - це набагато більш природний спосіб робити те, що я хочу. У підсумку я змінив прийняту відповідь
Michail Michailidis

Питання @IanS полягає в тому, як щось робиться в Angular, подібному до AngularJS? Чому назва вводить в оману?
Michail

Кутовий дуже відрізняється від AngularJS. Кутовий 2+ - це просто кутовий.
Ян S

1
Виправлено свою назву;)
Ian S

1
@IanS Дякую! Тепер питання про angularJs теж - з тегом, який ви додали.
Michail

Відповіді:


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. Отже, ось кутова документація для @Output для зацікавлених.
WebWanderer

9
Це добре для одностороннього зв’язування. Ви можете приєднатися до події дитини. Але ви не можете передавати дитині функцію зворотного дзвінка і дозволяти їй аналізувати зворотні значення зворотного дзвінка. Відповідь нижче дозволяє це.
грак

3
Я би сподівався отримати більше пояснень, чому віддавати перевагу одному способу проти іншого, а не "Я думаю, що це погане рішення".
Фідан Хакай

6
Можливо, це добре для 80% випадків, але не тоді, коли дочірній компонент хоче візуалізації залежно від того, чи існує зворотний виклик.
Джон Фріман,

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
Це спрацювало! Дякую! Я б хотів, щоб документація там десь була :)
Michail Michailidis

1
Ви можете використовувати статичний метод, якщо хочете, але тоді ви не мали б доступу до жодного з членів екземпляра компонента. Тож, мабуть, це не ваш випадок використання. Але так, вам також знадобиться передати це відParent -> Child
SnareChops

3
Чудова відповідь! Я, як правило, не перейменую функцію при прив'язці. в ngOnInitя б просто використовувати: this.theCallback = this.theCallback.bind(this)і тоді ви можете пройти разом, theCallbackа не theBoundCallback.
Зак

1
@MichailMichailidis Так, я погоджуюся з вашим рішенням та оновив свою відповідь приміткою, щоб привести людей до кращого шляху. Дякуємо, що стежили за цим.
SnareChops

7
@ Вихід та EventEmitter штрафують за один спосіб прив'язки. Ви можете приєднатись до події дитини, але ви не можете передати дитині функцію зворотного дзвінка і дозволити їй проаналізувати зворотне значення зворотного дзвінка. Ця відповідь дозволяє це.
грак

31

Альтернативу відповіді дав SnareChops.

Ви можете використовувати .bind (це) у своєму шаблоні, щоб мати такий же ефект. Це може бути не таким чистим, але це економить пару ліній. Я зараз на кутовому 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 повторно оцінює цей вираз при кожному виявленні змін. Інше рішення - виконувати прив'язку поза шаблоном - менш стисле, але у нього немає цієї проблеми.
Кріс

запитання: виконуючи .bind (це), ви прив'язуєте метод theCallBack з дитиною чи батьком? Я думаю, що це з дитиною. Але річ полягає в тому, що коли виклик посилається, це завжди називає дитина, тому ця в'язка не здається потрібною, якщо я прав.
ChrisZ

Він зв’язується з батьківським компонентом. Причина цього робиться в тому, що, коли викликається theCallBack (), він, ймовірно, захоче щось зробити всередині себе, і якщо "цей" не є батьківським компонентом, він буде поза контекстом і, отже, не може досягти власних методів та змінних більше.
Макс Фаль

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 (це), щоб утримувати потрібний контекст;
  2. Typesafe оголошення функції зворотного виклику в дочірньому компоненті.

1
Відмінне пояснення використання жирової стрілки для заміни використання.bind(this)
TYMG

6
Порада щодо використання: Не забудьте поставити, [getRowColor]="getColor"а не [getRowColor]="getColor()";-)
Simon_Weaver

Приємно. Це саме те, що я шукав. Простий та ефективний.
BrainSlugs83

7

Як приклад, я використовую модальне вікно для входу, де модальним вікном є батьківський, формою входу є дочірня і кнопка входу повертається до функції закриття модального батька.

Батьківський модал містить функцію закриття модалу. Цей батько передає функцію закриття до дочірнього компонента входу.

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 ?
Михайло

відповісти на цей коментар stackoverflow.com/questions/35328652 / ...
Shogg

0

Використовуйте спостережуваний візерунок. Ви можете помістити значення, що спостерігається (не Subject), в параметр Input і керувати ним з батьківського компонента. Вам не потрібна функція зворотного дзвінка.

Дивіться приклад: https://stackoverflow.com/a/49662611/4604351


чи можете ви проілюструвати це робочим прикладом?
Michail

0

Ще одна альтернатива.

ОП запитав спосіб використання зворотного дзвінка. У цьому випадку він мав на увазі конкретно функцію, яка обробляє подію (у його прикладі: подія клацання), яка трактується як прийнята відповідь від @serginho пропонує: з @Outputі EventEmitter.

Однак існує відмінність між зворотним дзвінком та подією: за допомогою зворотного дзвінка ваш дочірній компонент може отримати певний зворотній зв'язок або інформацію від батьків, але подія може лише повідомити, що щось сталося, не очікуючи зворотного зв'язку.

Є випадки використання, коли зворотний зв'язок необхідний, напр. отримати колір або список елементів, з якими компонент повинен обробляти. Ви можете використовувати зв'язані функції так, як пропонують деякі відповіді, або ви можете використовувати інтерфейси (це завжди мої переваги).

Приклад

Припустимо, у вас є загальний компонент, який працює над переліком елементів {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 (батьківський компонент) як об'єкт зворотного виклику. Ще однією перевагою є те, що не потрібно надсилати батьківський екземпляр, це може бути служба або будь-який об'єкт, що реалізує інтерфейс, якщо це дозволяє ваш випадок використання.

Повний приклад - на цьому стекбліці .


-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всередині функції зворотного виклику. Це просто схильність до помилок.
Олександр Юнгес

Ось приклад антикутника Angular 2.
Серхіньо

Це не повинно бути анти-зразком. Бувають випадки, коли ви хочете саме цього. Не так вже й рідко хочеться сказати компоненту, ЯК робити щось, що не стосується перегляду. Це має сенс, і я не бачу, чому ця відповідь набуває стільки ненависті.
Лазар Любенович
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.