Fonction de rappel de passage angulaire au composant enfant en tant que @Input similaire à la méthode AngularJS

228

AngularJS a les paramètres & où vous pouvez passer un rappel à une directive (par exemple, le mode de rappel AngularJS . Est-il possible de passer un rappel en tant que @Inputpour un composant angulaire (quelque chose comme ci-dessous)? Sinon, quelle serait la chose la plus proche de AngularJS le fait?

@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>
Michail Michailidis
la source
6
pour les futurs lecteurs, la @Inputmanière suggérée a rendu mon code spagetti et pas facile à entretenir .. @Outputs sont une façon beaucoup plus naturelle de faire ce que je veux. En conséquence, j'ai changé la réponse acceptée
Michail Michailidis
La question @IanS est de savoir comment quelque chose est fait dans Angular similaire à AngularJS? pourquoi le titre est-il trompeur?
Michail Michailidis
Angular est très différent d'AngularJS. Angular 2+ est juste angulaire.
Ian S
1
Correction de votre titre;)
Ian S
1
@IanS Merci! maintenant, la question concerne également les angularJ - avec la balise que vous avez ajoutée.
Michail Michailidis

Réponses:

297

Je pense que c'est une mauvaise solution. Si vous voulez passer une fonction en composant avec @Input(), le @Output()décorateur est ce que vous recherchez.

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
la source
45
Pour être précis, vous ne passez pas la fonction, mais connectez plutôt un écouteur d'événement d'écoute à la sortie. Utile pour comprendre pourquoi cela fonctionne.
Jens
13
C'est une excellente méthode, mais je me suis retrouvé avec beaucoup de questions après avoir lu cette réponse. J'espérais que ce serait plus en profondeur ou avoir un lien fourni décrivant @Outputet EventEmitter. Voici donc la documentation Angular pour @Output pour les personnes intéressées.
WebWanderer
9
C'est très bien pour la liaison unidirectionnelle. Vous pouvez vous connecter à l'événement de l'enfant. Mais vous ne pouvez pas passer une fonction de rappel à l'enfant et le laisser analyser la valeur de retour du rappel. La réponse ci-dessous le permet.
tour le
3
Je m'attendrais à avoir plus d'explications sur les raisons pour lesquelles préférer une manière plutôt qu'une autre au lieu d'avoir "Je pense que c'est une mauvaise solution".
Fidan Hakaj
6
Probablement bon pour 80% des cas, mais pas lorsqu'un composant enfant souhaite une visualisation conditionnelle à l'existence d'un rappel.
John Freeman
116

METTRE À JOUR

Cette réponse a été soumise alors que Angular 2 était encore en alpha et que de nombreuses fonctionnalités n'étaient pas disponibles / non documentées. Bien que ce qui suit fonctionnera toujours, cette méthode est désormais entièrement obsolète. Je recommande fortement la réponse acceptée ci-dessous.

Réponse originale

Oui, c'est le cas, mais vous voudrez vous assurer qu'il est correctement défini. Pour cela, j'ai utilisé une propriété pour garantir que cela thissignifie ce que je veux.

@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
la source
1
Ça a marché! Merci! Je souhaite que la documentation
contienne
1
Vous pouvez utiliser une méthode statique si vous le souhaitez, mais vous n'aurez alors accès à aucun des membres d'instance du composant. Ce n'est donc probablement pas votre cas d'utilisation. Mais oui, vous devrez également le transmettre à partir deParent -> Child
SnareChops
3
Très bonne réponse! Cependant, je ne renomme généralement pas la fonction lors de la liaison. dans ngOnInitje voudrais simplement utiliser: this.theCallback = this.theCallback.bind(this)et puis vous pouvez passer theCallbackau lieu de theBoundCallback.
Zack
1
@MichailMichailidis Oui, je suis d'accord avec votre solution et j'ai mis à jour ma réponse avec une note pour guider les gens vers la meilleure voie. Merci d'avoir gardé un œil sur celui-ci.
SnareChops
7
@Output et EventEmitter conviennent parfaitement pour une liaison à sens unique. Vous pouvez vous connecter à l'événement de l'enfant, mais vous ne pouvez pas passer une fonction de rappel à l'enfant et le laisser analyser la valeur de retour du rappel. Cette réponse le permet.
tour le
31

Une alternative à la réponse donnée par SnareChops.

Vous pouvez utiliser .bind (this) dans votre modèle pour avoir le même effet. Il n'est peut-être pas aussi propre, mais il enregistre quelques lignes. Je suis actuellement sur angular 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; 
  ...
}
Max Fahl
la source
2
comme d'autres l'ont commenté, bind (this) dans le modèle n'est documenté nulle part, il pourrait donc devenir obsolète / non pris en charge à l'avenir. De plus, @Inputle code devient spaghetti et l'utilisation des @Outputrésultats dans un processus plus naturel /
démêlé
1
Lorsque vous placez bind () dans le modèle, Angular réévalue cette expression à chaque détection de changement. L'autre solution - faire la liaison en dehors du modèle - est moins concise, mais elle n'a pas ce problème.
Chris
question: lorsque vous faites .bind (this), vous liez la méthode theCallBack avec l'enfant ou le parent? Je pense que c'est avec l'enfant. Mais le fait est que lorsque le lien est appelé, c'est toujours l'enfant qui l'appelle, donc ce lien ne semble pas nécessaire si j'ai raison.
ChrisZ
Il se lie au composant parent. La raison pour laquelle cela est fait est que lorsque theCallBack () est appelé, il voudra probablement faire quelque chose à l'intérieur de lui-même, et si "this" n'est pas le composant parent, il sera hors contexte et ne pourra donc pas atteindre ses propres méthodes et variables plus.
Max Fahl
29

Dans certains cas, vous devrez peut-être exécuter la logique métier par un composant parent. Dans l'exemple ci-dessous, nous avons un composant enfant qui rend la ligne du tableau en fonction de la logique fournie par le composant parent:

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

Donc, je voulais démontrer 2 choses ici:

  1. La grosse flèche (=>) fonctionne à la place de .bind (this) pour maintenir le bon contexte;
  2. Déclaration de typesafe d'une fonction de rappel dans le composant enfant.
Danylo Zatorsky
la source
1
Grande explication pour l'utilisation de la grosse flèche pour remplacer l'utilisation de.bind(this)
TYMG
6
Conseil d'utilisation: assurez-vous de mettre [getRowColor]="getColor"et non [getRowColor]="getColor()";-)
Simon_Weaver
Agréable. Ceci est exactement ce que je cherchais. Simple et efficace.
BrainSlugs83
7

À titre d'exemple, j'utilise une fenêtre modale de connexion, où la fenêtre modale est le parent, le formulaire de connexion est l'enfant et le bouton de connexion rappelle la fonction de fermeture du parent modal.

Le modal parent contient la fonction pour fermer le modal. Ce parent transmet la fonction close au composant enfant de connexion.

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

Une fois que le composant de connexion enfant a soumis le formulaire de connexion, il ferme le modal parent à l'aide de la fonction de rappel du parent

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;
  }
}
Camilla Kydland
la source
7

Une alternative à la réponse donnée par Max Fahl.

Vous pouvez définir la fonction de rappel comme une fonction de flèche dans le composant parent afin que vous n'ayez pas besoin de le lier.

@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
la source
5

Passer la méthode avec l'argument, en utilisant .bind à l'intérieur du modèle

@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
la source
Votre réponse n'est-elle pas essentiellement la même que celle-ci: stackoverflow.com/a/42131227/986160 ?
Michail Michailidis
en répondant à ce commentaire stackoverflow.com/questions/35328652/…
Shogg
0

Utilisez un motif observable. Vous pouvez mettre une valeur observable (non soumise) dans le paramètre d'entrée et la gérer à partir du composant parent. Vous n'avez pas besoin de fonction de rappel.

Voir l'exemple: https://stackoverflow.com/a/49662611/4604351

Alexey Baranoshnikov
la source
pouvez-vous l'illustrer avec un exemple de travail?
Michail Michailidis
0

Une autre alternative.

L'OP a demandé un moyen d'utiliser un rappel. Dans ce cas, il faisait spécifiquement référence à une fonction qui traite un événement (dans son exemple: un événement de clic), qui doit être traitée comme la réponse acceptée de @serginho le suggère: avec @Outputet EventEmitter.

Cependant, il existe une différence entre un rappel et un événement: avec un rappel, votre composant enfant peut récupérer des commentaires ou des informations du parent, mais un événement peut uniquement informer que quelque chose s'est produit sans attendre de retour.

Il existe des cas d'utilisation où une rétroaction est nécessaire, par ex. obtenir une couleur ou une liste d'éléments que le composant doit gérer. Vous pouvez utiliser des fonctions liées comme certaines réponses l'ont suggéré, ou vous pouvez utiliser des interfaces (c'est toujours ma préférence).

Exemple

Supposons que vous ayez un composant générique qui opère sur une liste d'éléments {id, nom} que vous souhaitez utiliser avec toutes vos tables de base de données qui contiennent ces champs. Cette composante devrait:

  • récupérer une gamme d'éléments (page) et les afficher dans une liste
  • permettre de supprimer un élément
  • informer qu'un élément a été cliqué, afin que le parent puisse prendre des mesures.
  • permet de récupérer la page suivante des éléments.

Composant enfant

En utilisant une liaison normale, nous aurions besoin de 1 @Input()et 3 @Output()paramètres (mais sans aucun retour du parent). Ex. <list-ctrl [items]="list" (itemClicked)="click($event)" (itemRemoved)="removeItem($event)" (loadNextPage)="load($event)" ...>, mais pour créer une interface, nous n'en aurons besoin que d'une seule @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); 
    }
}

Composant parent

Nous pouvons maintenant utiliser le composant liste dans le 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;
  }
}

Notez que le <list-ctrl>reçoit this(composant parent) en tant qu'objet de rappel. Un avantage supplémentaire est qu'il n'est pas nécessaire d'envoyer l'instance parente, il peut s'agir d'un service ou de tout objet qui implémente l'interface si votre cas d'utilisation le permet.

L'exemple complet se trouve sur ce stackblitz .

WPomier
la source
-3

La réponse actuelle peut être simplifiée en ...

@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; 
  ...
}
Bleu
la source
il n'est donc pas nécessaire de lier explicitement?
Michail Michailidis
3
Sans l' .bind(this)alors l' thisintérieur de la fonction de rappel sera ce windowqui peut ne pas fonctionner selon la matière sur votre cas d'utilisation. Cependant, si vous avez du tout thisdans le rappel, .bind(this)c'est nécessaire. Si vous ne le faites pas, cette version simplifiée est la voie à suivre.
SnareChops
3
Je recommande de toujours lier le rappel avec le composant, car vous finirez par utiliser thisà l'intérieur de la fonction de rappel. C'est juste sujet aux erreurs.
Alexandre Junges
C'est un exemple d'antipattern Angular 2.
Serginho
Il n'est pas nécessaire que ce soit un anti-modèle. Il y a des cas où vous voulez exactement cela. Il n'est pas rare de vouloir dire au composant COMMENT faire quelque chose qui ne concerne pas la vue. Cela a du sens et je ne vois pas pourquoi cette réponse suscite autant de haine.
Lazar Ljubenović