Функция обратного вызова углового прохода для дочернего компонента как @Input аналогично AngularJS

228

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предложенный способ сделал мой код спагетти и не простым в обслуживании. @OutputЭто гораздо более естественный способ делать то, что я хочу. В результате я изменил принятый ответ
Михаил Михайлидис
@IanS вопрос о том, как что-то делается в Angular, похожем на AngularJS? почему название вводит в заблуждение?
Михаил
Angular очень отличается от AngularJS. Angular 2+ это просто Angular.
Ян С.
1
Исправил ваш титул;)
Ян S
1
@IanS Спасибо! теперь вопрос тоже касается angularJs - с добавленным тэгом.
Михаил

Ответы:

297

Я думаю, что это плохое решение. Если вы хотите передать функцию в компонент @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>
Serginho
источник
45
Чтобы быть точным, вы не передаете функцию, а подключаете прослушиватель события слушателя к выходу. Полезно для понимания, почему это работает.
Йенс
13
Это отличный метод, но у меня осталось много вопросов после прочтения этого ответа. Я надеялся, что это будет более подробно или предоставит ссылку с описанием @Outputи EventEmitter. Итак, вот документация Angular для @Output для тех, кто заинтересован.
WebWanderer
9
Это хорошо для односторонней привязки. Вы можете подключиться к событию ребенка. Но вы не можете передать функцию обратного вызова дочернему элементу и позволить ей проанализировать возвращаемое значение обратного вызова. Ответ ниже позволяет это.
Ладья
3
Я хотел бы получить более подробное объяснение того, почему предпочтение одного способа другому, а не «я думаю, что это плохое решение».
Фидан Хакай,
6
Вероятно, хорошо для 80% случаев, но не тогда, когда дочерний компонент хочет, чтобы визуализация была обусловлена ​​наличием обратного вызова.
Джон Фриман
116

ОБНОВИТЬ

Этот ответ был представлен, когда 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; 
  ...
}
SnareChops
источник
1
Это сработало! Спасибо! Я бы хотел, чтобы в документации это было где-то :)
Михаил Михайлидис,
1
Вы можете использовать статический метод, если хотите, но тогда у вас не будет доступа ни к одному из элементов экземпляра компонента. Так что, вероятно, не ваш вариант использования. Но да, вам нужно было бы передать это также изParent -> Child
SnareChops
3
Отличный ответ! Я обычно не переименовываю функцию при связывании. в ngOnInitя бы просто использовать: this.theCallback = this.theCallback.bind(this)а затем вы можете передать theCallbackвместо theBoundCallback.
Зак
1
@MichailMichailidis Да, я согласен с вашим решением и обновил свой ответ примечанием, чтобы привести людей к лучшему пути. Спасибо, что следите за этим.
SnareChops
7
@Output и EventEmitter хороши для односторонней привязки. Вы можете подключиться к дочернему событию, но не можете передать функцию обратного вызова дочернему элементу и позволить ей проанализировать возвращаемое значение обратного вызова. Этот ответ позволяет это.
Ладья
31

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

Вы можете использовать .bind (this) в своем шаблоне, чтобы получить тот же эффект. Это может быть не так чисто, но это экономит пару строк. Я в настоящее время на угловой 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 переоценивает это выражение при каждом обнаружении изменений. Другое решение - сделать связывание вне шаблона - менее лаконично, но у него нет этой проблемы.
Крис
вопрос: при выполнении .bind (this) вы связываете метод theCallBack с дочерним или родительским? Я думаю, что это с ребенком. Но дело в том, что когда вызывается привязка, это всегда вызывает ребенок, так что эта привязка не кажется необходимой, если я прав.
ChrisZ
Связывается с родительским компонентом. Причина, по которой это делается, заключается в том, что, когда вызывается методCallBack (), он, вероятно, захочет что-то сделать внутри себя, и если «this» не является родительским компонентом, он будет вне контекста и, следовательно, не сможет получить доступ к своим собственным методам и переменным. больше.
Макс Фал
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 дочернему компоненту 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; 
  ...
}

jeadonara
источник
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; 

  ...
}
Shogg
источник
Разве ваш ответ по сути не такой, как этот: stackoverflow.com/a/42131227/986160 ?
Михаил
отвечая на этот комментарий stackoverflow.com/questions/35328652/…
Шогг
0

Используйте наблюдаемый шаблон. Вы можете поместить значение Observable (не Subject) во входной параметр и управлять им из родительского компонента. Вам не нужна функция обратного вызова.

Смотрите пример: https://stackoverflow.com/a/49662611/4604351

Алексей Бараношников
источник
Можете ли вы проиллюстрировать это на рабочем примере?
Михаил
0

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

ОП спросил способ использования обратного вызова. В этом случае он имел в виду конкретно функцию, которая обрабатывает событие (в его примере: событие щелчка), которое должно рассматриваться как принятый ответ от @serginho: with @Outputи EventEmitter.

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

Есть случаи, когда необходима обратная связь, напр. получить цвет или список элементов, которые компонент должен обработать. Вы можете использовать связанные функции, как предлагали некоторые ответы, или вы можете использовать интерфейсы (это всегда мое предпочтение).

пример

Предположим, у вас есть общий компонент, работающий со списком элементов {id, name}, который вы хотите использовать со всеми таблицами базы данных, в которых есть эти поля. Этот компонент должен:

  • получить диапазон элементов (страницы) и показать их в списке
  • разрешить удалить элемент
  • сообщить, что элемент был нажат, чтобы родитель мог предпринять некоторые действия.
  • Позволяет получить следующую страницу элементов.

Дочерний компонент

При обычном связывании нам понадобятся 1 @Input()и 3 @Output()параметра (но без обратной связи с родителем). Ex. <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); 
    }
}

Родительский компонент

Теперь мы можем использовать компонент list в родительском элементе.

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 (родительский компонент) как объект обратного вызова. Еще одно преимущество заключается в том, что отправка родительского экземпляра не требуется, это может быть служба или любой объект, который реализует интерфейс, если это позволяет ваш сценарий использования.

Полный пример этого стекаблита .

WPomier
источник
-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
3
Я рекомендую всегда связывать обратный вызов с компонентом, потому что в конечном итоге вы будете использовать thisвнутри функции обратного вызова. Это просто ошибка.
Александр Юнгес
Это пример Angular 2 антипаттерна.
Сержиньо,
Это не должно быть анти-паттерном. Есть случаи, когда вы хотите именно этого. Нередко хотеть сказать компоненту, КАК делать что-то, что не относится к представлению. Это имеет смысл, и я не понимаю, почему этот ответ становится настолько ненавистным.
Лазарь Любенович