Angular 2 Sibling Component Communication

Viewed 126242

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?

13 Answers

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.

Shared service is a good solution for this issue. If you want to store some activity information too, you can add Shared Service to your main modules (app.module) provider list.

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

Then you can directly provide it to your components,

constructor(private sharedService: SharedService)
 

With Shared Service you can either use functions or you can create a Subject to update multiple places at once.

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

In your list component you can publish clicked item information,

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

and then you can fetch this information at your detail component:

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

Obviously, the data that list component shares can be anything. Hope this helps.

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/

Behaviour subjects. I wrote a blog about that.

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

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

In this example i am declaring a noid behavior subject of type number. Also it is an observable. And if "something happend" this will change with the new(){} function.

So, in the sibling's components, one will call the function, to make the change, and the other one will be affected by that change, or vice-versa.

For example, I get the id from the URL and update the noid from the behavior subject.

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

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

And from the other side, I can ask if that ID is "what ever i want" and make a choice after that, in my case if i want to delte a task, and that task is the current url, it have to redirect me to the home:

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

One neat way of doing sibling to sibling communication is by using the @Output decorator in one child, and a template reference variable on the other child to let the parent invoke this child's methods. It's extremely similar to using @Ouput for child-parent-communication.

Doing this.emitSomething.emit(something); in child-2 will trigger onEmitSomething() in child-1.

child-1.component.ts

onEmitSomething(event: any): void {
  // do something
}

child-2.component.ts

@Output() emitSomething: EventEmitter<any> = new EventEmitter<any>();

parent.component.html

<child-1 #child1></child-1>
<child-2 (emitSomething)="child1.onEmitSomething($event)"></child-2>

The two different ways of component interaction can be found here Angular - Component Interaction

I also like to do the communication between 2 siblings via a parent component via input and output. it handles OnPush change notification better than using a common service. Or just use NgRx Store.

Example.

@Component({
    selector: 'parent',
    template: `<div><notes-grid 
            [Notes]="(NotesList$ | async)"
            (selectedNote)="ReceiveSelectedNote($event)"
        </notes-grid>
        <note-edit 
            [gridSelectedNote]="(SelectedNote$ | async)"
        </note-edit></div>`,
    styleUrls: ['./parent.component.scss']
})
export class ParentComponent {

    // create empty observable
    NotesList$: Observable<Note[]> = of<Note[]>([]);
    SelectedNote$: Observable<Note> = of<Note>();

    //passed from note-grid for selected note to edit.
    ReceiveSelectedNote(selectedNote: Note) {
    if (selectedNote !== null) {
        // change value direct subscribers or async pipe subscribers will get new value.
        this.SelectedNote$ = of<Note>(selectedNote);
    }
    }
    //used in subscribe next() to http call response.  Left out all that code for brevity.  This just shows how observable is populated.
    onNextData(n: Note[]): void {
    // Assign to Obeservable direct subscribers or async pipe subscribers will get new value.
    this.NotesList$ = of<Note[]>(n.NoteList);  //json from server
    }
}

//child 1 sibling
@Component({
  selector: 'note-edit',
  templateUrl: './note-edit.component.html', // just a textarea for noteText and submit and cancel buttons.
  styleUrls: ['./note-edit.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class NoteEditComponent implements OnChanges {
  @Input() gridSelectedNote: Note;

    constructor() {
    }

// used to capture @Input changes for new gridSelectedNote input
ngOnChanges(changes: SimpleChanges) {
     if (changes.gridSelectedNote && changes.gridSelectedNote.currentValue !== null) {      
      this.noteText = changes.gridSelectedNote.currentValue.noteText;
      this.noteCreateDtm = changes.gridSelectedNote.currentValue.noteCreateDtm;
      this.noteAuthorName = changes.gridSelectedNote.currentValue.noteAuthorName;
      }
  }

}

//child 2 sibling

@Component({
    selector: 'notes-grid',
    templateUrl: './notes-grid.component.html',  //just an html table with notetext, author, date
    styleUrls: ['./notes-grid.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotesGridComponent {

// the not currently selected fromt eh grid.
    CurrentSelectedNoteData: Note;

    // list for grid
    @Input() Notes: Note[];

    // selected note of grid sent out to the parent to send to sibling.
    @Output() readonly selectedNote: EventEmitter<Note> = new EventEmitter<Note>();

    constructor() {
    }

    // use when you need to send out the selected note to note-edit via parent using output-> input .
    EmitSelectedNote(){
    this.selectedNote.emit(this.CurrentSelectedNoteData);
    }

}


// here just so you can see what it looks like.

export interface Note {
    noteText: string;
    noteCreateDtm: string;
    noteAuthorName: string;
}
Related