Angular 2 Sibling Component Communication

Angular 2 Sibling Component Communication

I have a ListComponent. When an item is clicked in ListComponent, the details of that item should be shown in DetailComponent. Both are on the screen at the same time, so there’s no routing involved.
How do I tell DetailComponent what item in ListComponent was clicked?
I’ve considered emitting an event up to the parent (AppComponent), and have the parent set the selectedItem.id on DetailComponent with an @Input. Or I could use a shared service with observable subscriptions.

EDIT: Setting the selected item via event + @Input doesn’t trigger the DetailComponent, though, in case I were to need to execute additional code. So I’m not sure this is an acceptable solution.

But both of these methods seem far more complex than the Angular 1 way of doing things which was either through $rootScope.$broadcast or $scope.$parent.$broadcast.
With everything in Angular 2 being a component, I’m surprised there’s not more information out there about component communication.
Is there another/more straightforward way to accomplish this?

Solutions/Answers:

Solution 1:

Updated to rc.4:
When trying to get data passed between sibling components in angular 2, The simplest way right now (angular.rc.4) is to take advantage of angular2’s hierarchal dependency injection and create a shared service.

Here would be the service:

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

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

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

Now, here would be the PARENT component

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{

} 

and its two children

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

child 2 (It’s sibling)

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

NOW: Things to take note of when using this method.

  1. Only include the service provider for the shared service in the PARENT component and NOT the children.
  2. You still have to include constructors and import the service in the children
  3. This answer was originally answered for an early angular 2 beta version. All that has changed though are the import statements, so that is all you need to update if you used the original version by chance.

Solution 2:

In case of 2 different components (not nested components, parent\child\grandchild ) I suggest you this:

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

}

AstronautComponent:

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: Parent and children communicate via a service

Solution 3:

One way to do this is using a shared service.

However I find the following
solution much simpler, it allows to share data between 2 siblings.(I tested this only on Angular 5)

In you parent component template:

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

Solution 4:

There is a discussion about it here.

https://github.com/angular/angular.io/issues/2663

Alex J’s answer is good but it no longer works with current Angular 4 as of July, 2017.

And this plunker link would demonstrate how to communicate between siblings using shared service and observable.

https://embed.plnkr.co/P8xCEwSKgcOg07pwDrlO/

Solution 5:

A directive can make sense in certain situations to ‘connect’ components. In fact the things being connected don’t even need to be full components, and sometimes it’s more lightweight and actually simpler if they aren’t.

For example I’ve got a Youtube Player component (wrapping Youtube API) and I wanted some controller buttons for it. The only reason the buttons aren’t part of my main component is that they’re located elsewhere in the DOM.

In this case it’s really just an ‘extension’ component that will only ever be of use with the ‘parent’ component. I say ‘parent’, but in the DOM it is a sibling – so call it what you will.

Like I said it doesn’t even need to be a full component, in my case it’s just a <button> (but it could be a component).

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

In the HTML for ProductPage.component, where youtube-player is obviously my component that wraps the Youtube API.

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

... lots more DOM ...

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

The directive hooks everything up for me, and I don’t have to declare the (click) event in the HTML.

So the directive can nicely connect to the video player without having to involve ProductPage as a mediator.

This is the first time I’ve actually done this, so not yet sure how scalable it might be for much more complex situations. For this though I’m happy and it leaves my HTML simple and responsibilities of everything distinct.

Solution 6:

Here is simple practical explanation:Simply explained here

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

Component from where you want to call observable to inform another component that button is clicked

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

Component where you want to perform action on button clicked on another component

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

});

}

My understanding clear on components communication by reading this: http://musttoknow.com/angular-4-angular-5-communicate-two-components-using-observable-subject/