Dynamic Tab based application using Angular and Angular Material

Deepak Pandey
Nov 22, 2019 · 5 min read
Photo by Max Duzij on Unsplash

Today we will be developing a tab-based application using Angular and Angular Material.

Angular Material is a UI component library for Angular developers. Angular Material components help in constructing attractive, consistent, and functional web pages and web applications while adhering to modern web design principles like browser portability, device independence, and graceful degradation. It helps in creating faster, beautiful, and responsive websites. It is inspired by the Google Material Design.

Let us now start building the application from scratch.

Generating a Basic Angular Skeleton using Angular CLI

//Installs Angular CLI globally
npm install -g @angular/cli
//Generates an angular "tab-demo-app" app
ng new tab-demo-app
//Directory changed to the new app generated
cd tab-demo-app
//Installs Angular Material, the Component Dev Kit (CDK), Angular Animations and will ask you some questions to determine which features to include.
ng add @angular/material

Development

Whenever there is a change in the length of the tabs array that information is shared with the components via BehaviorSubject tabSub. The tabSub Subject passes the changed tabs array to the components who have subscribed to it. The code for the same is added below:

                         //tab.service.tsimport { Injectable } from "@angular/core";import { Tab } from "./tab.model";import { Comp1Component } from "./components/comp1.component";import { Comp2Component } from "./components/comp2.component";import { BehaviorSubject } from "rxjs";
@Injectable()export class TabService {public tabs: Tab[] = [new Tab(Comp1Component, "Comp1 View", { parent: "AppComponent" }),new Tab(Comp2Component, "Comp2 View", { parent: "AppComponent" })];public tabSub = new BehaviorSubject<Tab[]>(this.tabs);public removeTab(index: number) {this.tabs.splice(index, 1);if (this.tabs.length > 0) {this.tabs[this.tabs.length - 1].active = true;}this.tabSub.next(this.tabs);}public addTab(tab: Tab) {for (let i = 0; i < this.tabs.length; i++) {if (this.tabs[i].active === true) {this.tabs[i].active = false;}}tab.id = this.tabs.length + 1;tab.active = true;this.tabs.push(tab);this.tabSub.next(this.tabs);}}

Next, let us move to app.component.html. I have used Angular Material tabs here. To use the material tab elements we need to import MatTabsModule to AppModule. In the template, we are iterating over the tabs array using ngFor directive to generate multiple tabs.

The selectedIndex(input property of <mat-tab-group>) property is set to selectedTab variable. The tab with the set index(value of the selectedIndex) is made active in the browser.

selectedTabChange is the event that is fired when the active tab is changed.

                        //app.component.html<div class="container-fluid"><h1>Tab Demo Application</h1><button mat-stroked-button (click)="addNewTab()">Add new tab</button><mat-tab-groupclass="main-tab-group mt-2"[selectedIndex]="selectedTab"(selectedTabChange)="tabChanged($event)"><mat-tab *ngFor="let tab of tabs; let tabIndex = index"><ng-template mat-tab-label>{{ tab.title }}<i class="material-icons" (click)="removeTab(tabIndex)">close</i></ng-template><div><app-tab-content [tab]="tab"></app-tab-content></div></mat-tab></mat-tab-group></div>

AppComponent is the component that has subscribed to the Subject for the tabs in TabService. It has various methods like addTab(), removeTab() which further call the TabService methods to change the tabs array.

                      //app.component.tsimport { Component, OnInit } from "@angular/core";import { TabService } from "./tab.service";import { Tab } from "./tab.model";import { Comp1Component } from "./components/comp1.component";@Component({selector: "app-root",templateUrl: "./app.component.html",styleUrls: ["./app.component.css"]})export class AppComponent implements OnInit {tabs = new Array<Tab>();selectedTab: number;constructor(private tabService: TabService) {}ngOnInit() {this.tabService.tabSub.subscribe(tabs => {this.tabs = tabs;this.selectedTab = tabs.findIndex(tab => tab.active);});}tabChanged(event) {console.log("tab changed");}addNewTab() {this.tabService.addTab(new Tab(Comp1Component, "Comp1 View", { parent: "AppComponent" }));}removeTab(index: number): void {this.tabService.removeTab(index);}}

AppModule is the root module where all the components, directives are declared. It further imports the MatTabsModule which provides the tab related directives to the components which are part of this AppModule.

                        //app.module.tsimport { BrowserModule } from "@angular/platform-browser";import { NgModule } from "@angular/core";import { BrowserAnimationsModule } from "@angular/platform-browser/animations";import { MatTabsModule, MatButtonModule } from "@angular/material";import { AppComponent } from "./app.component";import { TabContentComponent } from "./tab-content.component";import { ContentContainerDirective } from "./content-container.directive";import { TabService } from "./tab.service";import { Comp1Component } from "./components/comp1.component";import { Comp2Component } from "./components/comp2.component";@NgModule({declarations: [AppComponent,TabContentComponent,ContentContainerDirective,Comp1Component,Comp2Component],imports: [BrowserModule,BrowserAnimationsModule,MatTabsModule,MatButtonModule],providers: [TabService],bootstrap: [AppComponent],entryComponents: [Comp1Component, Comp2Component]})export class AppModule {}

The below component’s template provides the container where dynamically the view will be loaded. Here the ContentContainerDirective injects ViewContainerRef to gain access to the view container of the element that will host the dynamically added component. The <ng-template> is the place where we add the directive

Further based on the component passed to the input object tab the ComponentFactoryResolver is used to resolve the ComponentFactory. The ComponentFactory is then used to create the instance of the component passed.

Using the ViewContainerRef reference we create the component view by using the createComponent() method and passing the ComponentFactory to it.

The createComponent() method returns a reference to the loaded component. We can then use that reference to interact with the component by assigning to its properties or calling its methods.

                   //tab-content.component.tsimport {Component,Input,ComponentFactoryResolver,ViewChild,OnInit} from "@angular/core";import { ContentContainerDirective } from "./content-container.directive";import { SkeletonComponent } from "./skeleton.component";import { Tab } from "./tab.model";@Component({selector: "app-tab-content",template: "<ng-template content-container></ng-template>"})export class TabContentComponent implements OnInit {@Input() tab;@ViewChild(ContentContainerDirective, { static: true })contentContainer: ContentContainerDirective;constructor(private componentFactoryResolver: ComponentFactoryResolver) {}ngOnInit() {const tab: Tab = this.tab;
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(tab.component;
const viewContainerRef = this.contentContainer.viewContainerRef;const componentRef = viewContainerRef.createComponent(componentFactory);(componentRef.instance as SkeletonComponent).data = tab.tabData;}}

ContentContainerDirective is the anchor directive that defines the point where we can inject the component view.

                //content-container.directive.tsimport { Directive, ViewContainerRef } from '@angular/core';@Directive({selector: "[content-container]"})export class ContentContainerDirective {constructor(public viewContainerRef: ViewContainerRef) { }}

Comp1Component and Comp2Component are the two components that will be dynamically created and are part of entryComponents array in AppModule.

                    //comp1.component.tsimport { Component, Input } from "@angular/core";@Component({template: "<div> <p>I am comp1 loaded by {{data.parent}}<p></div>"})export class Comp1Component {@Input() data;}                     //comp2.component.tsimport { Component, Input } from "@angular/core";@Component({template: "<div> <p>I am comp2 loaded by {{data.parent}}<p></div>"})export class Comp2Component {@Input() data;}

This is the tab model class that holds the tab data.

                         //tab.model.tsimport { Type } from '@angular/core';export class Tab {public id: number;public title: string;public tabData: any;public active: boolean;public component: Type<any>;constructor(component: Type<any>, title: string, tabData: any) {this.tabData = tabData;this.component = component;this.title = title;}}
Application Demo in Browser

You can find the complete code for the same in the repo link mentioned below:

References:

That is all I have for now. Thank you for reading. … :)

JavaScript in Plain English

Learn the web's most important programming language.

Deepak Pandey

Written by

Angular | Vue | Angular JS | JavaScript | Python | Java

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade