Angular pass callback function to child component as @Input similar to AngularJS way

后端 未结 10 1524
庸人自扰
庸人自扰 2020-11-27 10:22

AngularJS has the & parameters where you could pass a callback to a directive (e.g AngularJS way of callbacks. Is it possible to pass a callback as an @Input

相关标签:
10条回答
  • 2020-11-27 10:28

    Another alternative.

    The OP asked a way to use a callback. In this case he was referring specifically to a function that process an event (in his example: a click event), which shall be treated as the accepted answer from @serginho suggests: with @Output and EventEmitter.

    However, there is a difference between a callback and an event: With a callback your child component can retrieve some feedback or information from the parent, but an event only can inform that something happened without expect any feedback.

    There are use cases where a feedback is necessary, ex. get a color, or a list of elements that the component needs to handle. You can use bound functions as some answers have suggested, or you can use interfaces (that's always my preference).

    Example

    Let's suppose you have a generic component that operates over a list of elements {id, name} that you want to use with all your database tables that have these fields. This component should:

    • retrieve a range of elements (page) and show them in a list
    • allow remove an element
    • inform that an element was clicked, so the parent can take some action(s).
    • allow retrieve the next page of elements.

    Child Component

    Using normal binding we would need 1 @Input() and 3 @Output() parameters (but without any feedback from the parent). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, but creating an interface we will need only one @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); 
        }
    }
    

    Parent Component

    Now we can use the list component in the parent.

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

    Note that the <list-ctrl> receives this (parent component) as the callback object. One additional advantage is that it's not required to send the parent instance, it can be a service or any object that implements the interface if your use case allows it.

    The complete example is on this stackblitz.

    0 讨论(0)
  • 2020-11-27 10:33

    In some cases, you might need business logic to be performed by a parent component. In the example below we have a child component that renders table row depending on the logic provided by the parent component:

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

    So, I wanted to demonstrate 2 things here:

    1. Fat arrow (=>) functions instead of .bind(this) to hold the right context;
    2. Typesafe declaration of a callback function in the child component.
    0 讨论(0)
  • 2020-11-27 10:33

    As an example, I am using a login modal window, where the modal window is the parent, the login form is the child and the login button calls back to the modal parent's close function.

    The parent modal contains the function to close the modal. This parent passes the close function to the login child component.

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

    After the child login component submits the login form, it closes the parent modal using the parent's callback function

    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;
      }
    }
    
    0 讨论(0)
  • 2020-11-27 10:34

    An alternative to the answer Max Fahl gave.

    You can define callback function as an arrow function in the parent component so that you won't need to bind that.

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

    0 讨论(0)
  • 2020-11-27 10:35

    The current answer can be simplified to...

    @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; 
      ...
    }
    
    0 讨论(0)
  • 2020-11-27 10:37

    I think that is a bad solution. If you want to pass a Function into component with @Input(), @Output() decorator is what you are looking for.

    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>
    
    0 讨论(0)
提交回复
热议问题