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:
- GitHub: angular-wijmo-flexsheet
References:
- Angular Docs: Component Interaction
Additional Resources:
- Compodoc: Getting Started Guide
- wijmo 5 Forum: FlexGrid Import/Export to Excel using JSZip in Webpack