Angular 4 and Sibling Component Interaction

Angular's Getting Started Guide contains recipes for common component communication scenarios in which two or more components share information.

The scenarios discussed, include:

  • Pass data from parent to child with input binding
  • Intercept input property changes with a setter
  • Intercept input property changes with ngOnChanges()
  • Parent listens for child event
  • Parent interacts with child via local variable
  • Parent calls an @ViewChild()
  • Parent and children communicate via a service

Each scenario discusses parent -> child or child -> parent interaction.

In this post, I'll walk you through the steps I followed to enable sibling component interaction.

Excel-like UI

In the previous post, I wrote about the steps I followed when creating an application with an Excel-like UI.

The following regions compose Excel's UI:

  • Quick Access Toolbar
  • Ribbon (including Ribbon Tabs)
  • Formula Bar
  • Worksheet Grid
  • Worksheet Tabs
  • Status Bar

At this point, the application has a Ribbon component, a Formula Bar and a Worksheet component:

The Application Component

Let's take a look at the template for the AppComponent (app.component.html):

<div class="excelbook" style="position: relative; overflow-y: hidden;" (window:resize)="onResize($event)">

  <div id="ribbon-tabs-container">
    <app-ribbon (ribbonClicked)="ribbonClicked($event)"></app-ribbon>
  </div>

  <div id="worksheet-container" [style.height.px]="worksheetHeight">
    <app-worksheet></app-worksheet>
  </div>

</div>

The AppComponent (the parent) has two child components: the RibbonComponent (<app-ribbon>); and the WorksheetComponent (<app-worksheet>). The RibbonComponent and the WorksheetComponent are siblings:

The Ribbon Component

Let's take a look at the template for the RibbonComponent (ribbon.component.html):

<div class="row ribbon-container">

  <!-- File Panel -->
  <div class="panel panel-default">
    <div class="panel-body">
      <div class="btn-group">

        ...

        <div class="btn btn-default btn-large no-border">
          <span class="glyphicon glyphicon-open"></span>
          <span class="text">Load</span>
          <input type="file" class="load"
          (change)="command({ methodName: 'fileLoad', param1: $event })"
          accept="application/vnd.openxmlformats-
            officedocument.spreadsheetml.sheet, 
          application/vnd.ms-excel.sheet.macroEnabled.12" />
        </div>
      </div>
    </div>
    <div class="panel-footer text-center">File</div>
  </div>
  
  ...
  
</div>

When a user click's the 'Load' button the RibbonComponent's command() method is invoked:

export class RibbonComponent {

  @Output() ribbonClicked = new EventEmitter<any>();

  command(action: any) {
    this.ribbonClicked.emit(action);
  }
}

And the RibbonComponent emits a ribbonClicked event that the parent (the AppComponent) binds to:

  <div id="ribbon-tabs-container">
    <app-ribbon (ribbonClicked)="ribbonClicked($event)"></app-ribbon>
  </div>

The parent (the AppComponent) inject's a child component (the WorksheetComponent) as a @ViewChild so that it can react to the ribbonClicked event:

export class AppComponent implements OnInit {

  @ViewChild(WorksheetComponent)
  private worksheet: WorksheetComponent;
  
  ...
  
  ribbonClicked(event: any) {

    const methodName = event.methodName;

    if (this.worksheet[methodName]) {
      if (event.hasOwnProperty('param1')) {
        const param1 = event.param1;
        this.worksheet[methodName](param1);
      } else {
        this.worksheet[methodName]();
      }
    }
  }
}  

By invoking a WorksheetComponent method, for example, fileLoad():

  fileLoad(event: any) {
    if (this.flexSheet && event.target.files[0]) {
      this.flexSheet.loadAsync(event.target.files[0]);
      event.target.value = '';
    }
  }

You should see output like:

In the scenario discussed above, we have enabled sibling component interaction (albeit indirectly) by utilising the Parent listens for child event and the Parent calls an @ViewChild() component communication scenarios.

Service, Observable and a Subject

Another approach to sibling component interaction is to use a Service, an Observable and a Subject.

The Worksheet Service

Let's take a look at the Worksheet Service (worksheet.service.ts):

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

@Injectable()
export class WorksheetService {

  private selectionFormatState = new Subject<any>();

  setState(state: any) {
    this.selectionFormatState.next(state);
  }

  getState(): Observable<any> {
    return this.selectionFormatState.asObservable();
  }
}

The Worksheet Component

The WorksheetComponent can use the WorksheetService's setState() method to share state information:

import * as wjcGridSheet from 'wijmo/wijmo.grid.sheet';
import { WorksheetService } from '../services/worksheet.service';

export class WorksheetComponent {

  @ViewChild('flexSheet')
  private flexSheet: wjcGridSheet.FlexSheet;
  
  selectionFormatState: wjcGridSheet.IFormatState = {};

  constructor(private worksheetService: WorksheetService) {
  }
  
  applyBoldStyle() {
    if (this.flexSheet) {
      this.flexSheet.applyCellsStyle({ fontWeight: this.selectionFormatState.isBold ? 'none' : 'bold' });
      this.selectionFormatState.isBold = !this.selectionFormatState.isBold;
      this.worksheetService.setState(this.selectionFormatState);
    }
  }
}

The Ribbon Component

The RibbonComponent can use the WorksheetService's getState() method to obtain (WorksheetComponent) state information:

import { WorksheetService } from '../services/worksheet.service';
import { Subscription } from 'rxjs/Subscription';

export class RibbonComponent implements OnDestroy {

  private subscription: Subscription;
  selectionFormatState: any = {};

  constructor(private worksheetService: WorksheetService) {
    this.subscription = this.worksheetService.getState().subscribe(
      selectionFormatState => {
        this.selectionFormatState = selectionFormatState;
      });
  }
  
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Now we can use the selectionFormatState to toggle the Ribbon buttons active state:

  <!-- Font Panel -->
  <div class="panel panel-default">
    <div class="panel-body">
      <div class="btn-group-vertical">
        <div class="btn-group btn-group-h">
          <button type="button" class="btn btn-default btn-small {{selectionFormatState.isBold ? 'active' : ''}}"
                  title="Bold" (click)="command({ methodName: 'applyBoldStyle' })">
            <span class="glyphicon glyphicon-bold"></span>
            <span class="text">Bold</span>
          </button>
          
          ...
          
        </div>
      </div>
    </div>
    <div class="panel-footer text-center">Font</div>
  </div>          

The Ribbon's buttons reflect the WorksheetComponent's selectionFormatState:

Source Code:
References:
Additional Resources: