Communication des composants angulaires 2 frères et sœurs


118

J'ai un ListComponent. Lorsqu'un élément est cliqué dans ListComponent, les détails de cet élément doivent être affichés dans DetailComponent. Les deux sont à l'écran en même temps, il n'y a donc pas de routage impliqué.

Comment puis-je dire à DetailComponent quel élément de ListComponent a été cliqué?

J'ai envisagé d'émettre un événement jusqu'au parent (AppComponent) et j'ai demandé au parent de définir selectedItem.id sur DetailComponent avec un @Input. Ou je pourrais utiliser un service partagé avec des abonnements observables.


EDIT: La définition de l'élément sélectionné via event + @Input ne déclenche pas le DetailComponent, cependant, au cas où je devrais exécuter du code supplémentaire. Je ne suis donc pas sûr que ce soit une solution acceptable.


Mais ces deux méthodes semblent beaucoup plus complexes que la manière angulaire 1 de faire les choses qui était soit via $ rootScope. $ Broadcast ou $ scope. $ Parent. $ Broadcast.

Tout dans Angular 2 étant un composant, je suis surpris qu'il n'y ait pas plus d'informations sur la communication des composants.

Existe-t-il un autre moyen / plus simple d'accomplir cela?


Avez-vous trouvé un moyen de partager des données entre frères et sœurs? J'en ai besoin comme observable ..
Human Being

Réponses:


65

Mise à jour vers rc.4: lorsque vous essayez de faire passer des données entre des composants frères dans angular 2, le moyen le plus simple à l'heure actuelle (angular.rc.4) est de tirer parti de l'injection de dépendances hiérarchiques d'angular2 et de créer un service partagé.

Voici le service:

import {Injectable} from '@angular/core';

@Injectable()
export class SharedService {
    dataArray: string[] = [];

    insertData(data: string){
        this.dataArray.unshift(data);
    }
}

Maintenant, voici le composant PARENT

import {Component} from '@angular/core';
import {SharedService} from './shared.service';
import {ChildComponent} from './child.component';
import {ChildSiblingComponent} from './child-sibling.component';
@Component({
    selector: 'parent-component',
    template: `
        <h1>Parent</h1>
        <div>
            <child-component></child-component>
            <child-sibling-component></child-sibling-component>
        </div>
    `,
    providers: [SharedService],
    directives: [ChildComponent, ChildSiblingComponent]
})
export class parentComponent{

} 

et ses deux enfants

enfant 1

import {Component, OnInit} from '@angular/core';
import {SharedService} from './shared.service'

@Component({
    selector: 'child-component',
    template: `
        <h1>I am a child</h1>
        <div>
            <ul *ngFor="#data in data">
                <li>{{data}}</li>
            </ul>
        </div>
    `
})
export class ChildComponent implements OnInit{
    data: string[] = [];
    constructor(
        private _sharedService: SharedService) { }
    ngOnInit():any {
        this.data = this._sharedService.dataArray;
    }
}

enfant 2 (c'est frère)

import {Component} from 'angular2/core';
import {SharedService} from './shared.service'

@Component({
    selector: 'child-sibling-component',
    template: `
        <h1>I am a child</h1>
        <input type="text" [(ngModel)]="data"/>
        <button (click)="addData()"></button>
    `
})
export class ChildSiblingComponent{
    data: string = 'Testing data';
    constructor(
        private _sharedService: SharedService){}
    addData(){
        this._sharedService.insertData(this.data);
        this.data = '';
    }
}

MAINTENANT: Points à prendre en compte lors de l'utilisation de cette méthode.

  1. N'incluez que le fournisseur de services pour le service partagé dans le composant PARENT et PAS les enfants.
  2. Vous devez toujours inclure des constructeurs et importer le service dans les enfants
  3. Cette réponse a été initialement répondue pour une version bêta angulaire 2 précoce. Tout ce qui a changé, ce sont les instructions d'importation, c'est donc tout ce que vous devez mettre à jour si vous avez utilisé la version d'origine par hasard.

2
Est-ce toujours valable pour angular-rc1?
Sergio

4
Je ne pense pas que cela informe le frère que quelque chose a été mis à jour dans le service partagé, cependant. Si child-component1 fait quelque chose auquel child-component2 doit répondre, cette méthode ne gère pas cela. Je crois que le moyen de contourner cela est avec les observables?
dennis.sheppard

1
@Sufyan: Je suppose que l'ajout des champs de fournisseur aux enfants amène Angular à créer de nouvelles instances privées pour chacun. Lorsque vous ne les ajoutez pas, ils utilisent l'instance "singleton" du parent.
Ralph

1
On dirait que cela ne fonctionne plus avec les dernières mises à jour
Sufyan Jabr

3
C'est obsolète. directivesne sont plus déclarées dans le composant.
Nate Gardner

26

Dans le cas de 2 composants différents (composants non imbriqués, parent \ enfant \ petit-enfant) je vous suggère ceci:

MissionService:

import { Injectable } from '@angular/core';
import { Subject }    from 'rxjs/Subject';

@Injectable()

export class MissionService {
  // Observable string sources
  private missionAnnouncedSource = new Subject<string>();
  private missionConfirmedSource = new Subject<string>();
  // Observable string streams
  missionAnnounced$ = this.missionAnnouncedSource.asObservable();
  missionConfirmed$ = this.missionConfirmedSource.asObservable();
  // Service message commands
  announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
  }
  confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
  }

}

Astronaute Composant:

import { Component, Input, OnDestroy } from '@angular/core';
import { MissionService } from './mission.service';
import { Subscription }   from 'rxjs/Subscription';
@Component({
  selector: 'my-astronaut',
  template: `
    <p>
      {{astronaut}}: <strong>{{mission}}</strong>
      <button
        (click)="confirm()"
        [disabled]="!announced || confirmed">
        Confirm
      </button>
    </p>
  `
})
export class AstronautComponent implements OnDestroy {
  @Input() astronaut: string;
  mission = '<no mission announced>';
  confirmed = false;
  announced = false;
  subscription: Subscription;
  constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
      mission => {
        this.mission = mission;
        this.announced = true;
        this.confirmed = false;
    });
  }
  confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
  }
  ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
  }
}

Source: les parents et les enfants communiquent via un service


2
J'aimerais que vous ajoutiez de la terminologie à cette réponse. Je crois que cela correspond à RxJS, modèle observable, etc. pas tout à fait sûr, mais ajouter des descriptions à certains de ces éléments serait bénéfique pour les gens (comme moi).
karns

13

Une façon de procéder consiste à utiliser un service partagé .

Cependant, je trouve la solution suivante beaucoup plus simple, elle permet de partager des données entre 2 frères et sœurs (je l'ai testé uniquement sur Angular 5 )

Dans votre modèle de composant parent:

<!-- Assigns "AppSibling1Component" instance to variable "data" -->
<app-sibling1 #data></app-sibling1>
<!-- Passes the variable "data" to AppSibling2Component instance -->
<app-sibling2 [data]="data"></app-sibling2> 

app-sibling2.component.ts

import { AppSibling1Component } from '../app-sibling1/app-sibling1.component';
...

export class AppSibling2Component {
   ...
   @Input() data: AppSibling1Component;
   ...
}

N'est-ce pas contraire à l'idée de couplage lâche et donc de composants?
Robin

Quelqu'un a-t-il une idée si c'est une façon propre ou sale de s'y prendre? Cela semble beaucoup plus simple pour partager des données dans une direction, par exemple uniquement de sibiling1 à sibiling2, mais pas l'inverse
Sarah


7

Une directive peut avoir un sens dans certaines situations pour «connecter» des composants. En fait, les éléments connectés n'ont même pas besoin d'être des composants complets, et parfois c'est plus léger et en fait plus simple s'ils ne le sont pas.

Par exemple, j'ai un Youtube Playercomposant (enveloppant l'API Youtube) et je voulais des boutons de contrôleur pour cela. La seule raison pour laquelle les boutons ne font pas partie de mon composant principal est qu'ils sont situés ailleurs dans le DOM.

Dans ce cas, il s'agit en réalité d'un composant «extension» qui ne sera jamais utilisé qu'avec le composant «parent». Je dis «parent», mais dans le DOM, c'est un frère - alors appelez-le comme vous voulez.

Comme je l'ai dit, il n'est même pas nécessaire que ce soit un composant complet, dans mon cas, c'est juste un <button>(mais cela pourrait être un composant).

@Directive({
    selector: '[ytPlayerPlayButton]'
})
export class YoutubePlayerPlayButtonDirective {

    _player: YoutubePlayerComponent; 

    @Input('ytPlayerVideo')
    private set player(value: YoutubePlayerComponent) {
       this._player = value;    
    }

    @HostListener('click') click() {
        this._player.play();
    }

   constructor(private elementRef: ElementRef) {
       // the button itself
   }
}

Dans le HTML pour ProductPage.component, où youtube-playerest évidemment mon composant qui encapsule l'API Youtube.

<youtube-player #technologyVideo videoId='NuU74nesR5A'></youtube-player>

... lots more DOM ...

<button class="play-button"        
        ytPlayerPlayButton
        [ytPlayerVideo]="technologyVideo">Play</button>

La directive relie tout pour moi, et je n'ai pas à déclarer l'événement (click) dans le HTML.

Ainsi, la directive peut bien se connecter au lecteur vidéo sans avoir à s'impliquer en ProductPagetant que médiateur.

C'est la première fois que je fais cela, donc je ne sais pas encore à quel point cela pourrait être évolutif pour des situations beaucoup plus complexes. Pour cela, je suis heureux et cela laisse mon HTML simple et les responsabilités de tout distinctes.


L'un des concepts angulaires les plus importants à comprendre est qu'un composant n'est qu'une directive avec un modèle. Une fois que vous avez vraiment compris ce que cela signifie, les directives ne sont pas si effrayantes - et vous réaliserez que vous pouvez les appliquer à n'importe quel élément pour y attacher un comportement.
Simon_Weaver

J'ai essayé cela mais j'obtiens une erreur d'identifiant en double pour l'équivalent de player. Si je laisse de côté la première mention du joueur, j'obtiens une rangeError. Je suis perplexe quant à la façon dont cela est censé fonctionner.
Katharine Osborne

@KatharineOsborne On dirait que dans mon code actuel, j'utilise _playerpour le champ privé qui représente le joueur, donc oui, vous obtiendrez une erreur si vous copiez cela exactement. Mettra à jour. Désolé!
Simon_Weaver

4

Voici une explication pratique simple: Simplement expliqué ici

Dans call.service.ts

import { Observable } from 'rxjs';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class CallService {
 private subject = new Subject<any>();

 sendClickCall(message: string) {
    this.subject.next({ text: message });
 }

 getClickCall(): Observable<any> {
    return this.subject.asObservable();
 }
}

Composant à partir duquel vous souhaitez appeler observable pour informer un autre composant que le bouton a été cliqué

import { CallService } from "../../../services/call.service";

export class MarketplaceComponent implements OnInit, OnDestroy {
  constructor(public Util: CallService) {

  }

  buttonClickedToCallObservable() {
   this.Util.sendClickCall('Sending message to another comp that button is clicked');
  }
}

Composant sur lequel vous souhaitez effectuer une action sur le bouton cliqué sur un autre composant

import { Subscription } from 'rxjs/Subscription';
import { CallService } from "../../../services/call.service";


ngOnInit() {

 this.subscription = this.Util.getClickCall().subscribe(message => {

 this.message = message;

 console.log('---button clicked at another component---');

 //call you action which need to execute in this component on button clicked

 });

}

import { Subscription } from 'rxjs/Subscription';
import { CallService } from "../../../services/call.service";


ngOnInit() {

 this.subscription = this.Util.getClickCall().subscribe(message => {

 this.message = message;

 console.log('---button clicked at another component---');

 //call you action which need to execute in this component on button clicked

});

}

Ma compréhension claire de la communication des composants en lisant ceci: http://musttoknow.com/angular-4-angular-5-communicate-two-components-using-observable-subject/


Hé merci beaucoup pour la solution simple> je l'ai essayé dans stackblitz qui a bien fonctionné. Mais mon application a des routes chargées paresseusement (ont utilisé providedin: 'root') et des appels HTTP pour définir et obtenir. Pourriez-vous m'aider avec les appels HTTP. essayé beaucoup mais ne fonctionne pas:
Kshri

4

Le service partagé est une bonne solution à ce problème. Si vous souhaitez également stocker des informations d'activité, vous pouvez ajouter Shared Service à votre liste de fournisseurs de modules principaux (app.module).

@NgModule({
    imports: [
        ...
    ],
    bootstrap: [
        AppComponent
    ],
    declarations: [
        AppComponent,
    ],
    providers: [
        SharedService,
        ...
    ]
});

Ensuite, vous pouvez le fournir directement à vos composants,

constructor(private sharedService: SharedService)

Avec Shared Service, vous pouvez utiliser des fonctions ou créer un objet pour mettre à jour plusieurs emplacements à la fois.

@Injectable()
export class FolderTagService {
    public clickedItemInformation: Subject<string> = new Subject(); 
}

Dans votre composant de liste, vous pouvez publier des informations sur les éléments cliqués,

this.sharedService.clikedItemInformation.next("something");

puis vous pouvez récupérer ces informations dans votre composant de détail:

this.sharedService.clikedItemInformation.subscribe((information) => {
    // do something
});

De toute évidence, les données qui répertorient les partages de composants peuvent être n'importe quoi. J'espère que cela t'aides.


C'est l'exemple le plus simple (aka laconique) de ce concept de service partagé, et devrait vraiment être voté pour augmenter sa visibilité, car il n'y a pas de réponse acceptée.
iGanja

3

Vous devez configurer la relation parent-enfant entre vos composants. Le problème est que vous pouvez simplement injecter les composants enfants dans le constructeur du composant parent et le stocker dans une variable locale. Au lieu de cela, vous devez déclarer les composants enfants dans votre composant parent à l'aide du @ViewChilddéclarateur de propriété. Voici à quoi devrait ressembler votre composant parent:

import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ListComponent } from './list.component';
import { DetailComponent } from './detail.component';

@Component({
  selector: 'app-component',
  template: '<list-component></list-component><detail-component></detail-component>',
  directives: [ListComponent, DetailComponent]
})
class AppComponent implements AfterViewInit {
  @ViewChild(ListComponent) listComponent:ListComponent;
  @ViewChild(DetailComponent) detailComponent: DetailComponent;

  ngAfterViewInit() {
    // afther this point the children are set, so you can use them
    this.detailComponent.doSomething();
  }
}

https://angular.io/docs/ts/latest/api/core/index/ViewChild-var.html

https://angular.io/docs/ts/latest/cookbook/component-communication.html#parent-to-view-child

Attention, le composant enfant ne sera pas disponible dans le constructeur du composant parent, juste après l' ngAfterViewInitappel du hook de cycle de vie. Pour attraper ce hook, implémentez simplement l' AfterViewInitinterface dans votre classe parent de la même manière que vous le feriez avec OnInit.

Mais, il existe d'autres déclarateurs de propriété comme expliqué dans cette note de blog: http://blog.mgechev.com/2016/01/23/angular2-viewchildren-contentchildren-difference-viewproviders/


2

Sujets de comportement. J'ai écrit un blog à ce sujet.

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
private noId = new BehaviorSubject<number>(0); 
  defaultId = this.noId.asObservable();

newId(urlId) {
 this.noId.next(urlId); 
 }

Dans cet exemple, je déclare un sujet de comportement noid de type number. C'est aussi un observable. Et si "quelque chose s'est passé", cela changera avec la fonction new () {}.

Ainsi, dans les composants du frère, l'un appellera la fonction, pour effectuer le changement, et l'autre sera affecté par ce changement, ou vice-versa.

Par exemple, j'obtiens l'identifiant de l'URL et mets à jour le noid du sujet de comportement.

public getId () {
  const id = +this.route.snapshot.paramMap.get('id'); 
  return id; 
}

ngOnInit(): void { 
 const id = +this.getId ();
 this.taskService.newId(id) 
}

Et de l'autre côté, je peux demander si cet identifiant est "ce que je veux" et faire un choix après cela, dans mon cas si je veux supprimer une tâche, et cette tâche est l'url actuelle, elle doit me rediriger à la maison:

delete(task: Task): void { 
  //we save the id , cuz after the delete function, we  gonna lose it 
  const oldId = task.id; 
  this.taskService.deleteTask(task) 
      .subscribe(task => { //we call the defaultId function from task.service.
        this.taskService.defaultId //here we are subscribed to the urlId, which give us the id from the view task 
                 .subscribe(urlId => {
            this.urlId = urlId ;
                  if (oldId == urlId ) { 
                // Location.call('/home'); 
                this.router.navigate(['/home']); 
              } 
          }) 
    }) 
}

1

Ce n'est pas exactement ce que vous voulez, mais cela vous aidera à coup sûr

Je suis surpris qu'il n'y ait pas plus d'informations sur la communication des composants <=> considérez ce tutoriel par angualr2

Pour la communication des composants frères et sœurs, je suggérerais d'aller avec sharedService. Il existe également d'autres options disponibles.

import {Component,bind} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {HTTP_PROVIDERS} from 'angular2/http';
import {NameService} from 'src/nameService';


import {TheContent} from 'src/content';
import {Navbar} from 'src/nav';


@Component({
  selector: 'app',
  directives: [TheContent,Navbar],
  providers: [NameService],
  template: '<navbar></navbar><thecontent></thecontent>'
})


export class App {
  constructor() {
    console.log('App started');
  }
}

bootstrap(App,[]);

Veuillez vous référer au lien en haut pour plus de code.

Edit: Ceci est une toute petite démo. Vous avez déjà mentionné que vous avez déjà essayé avec sharedService. Veuillez donc considérer ce tutoriel par angualr2 pour plus d'informations.


0

J'ai transmis des méthodes setter du parent à l'un de ses enfants via une liaison, appelant cette méthode avec les données du composant enfant, ce qui signifie que le composant parent est mis à jour et peut ensuite mettre à jour son deuxième composant enfant avec les nouvelles données. Cela nécessite une liaison «this» ou l'utilisation d'une fonction de flèche.

Cela présente l'avantage que les enfants ne sont pas tellement couplés les uns aux autres car ils n'ont pas besoin d'un service partagé spécifique.

Je ne suis pas tout à fait sûr que ce soit la meilleure pratique, il serait intéressant d'entendre les opinions des autres à ce sujet.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.