Dynamic tabs in angular (eager/early loading) with managed state


Hey guys, in this post we are going to see how to create tabs dynamically in which each tab will contain data from each component. There are many ways to create tab structure, in this post we are going to focus on eagerly loaded component, which means all components that will act as a tab will be loaded in advance when the tab holder component is loaded. Let’s do it step by step.
If you want lazy loading to load the tabs, please visit this post click here.

[at the bottom of the post you will get the link to download the project]
[ If you are preparing for an interview please click here - angular interview questions and answers ]

At very first, install angular material and angular CDK to support material.
Following is the installation command and description.



  
D:\Angular-DemoApps\Angular 6 - Dynamic tab implementation with Multiple component (Component as a tab, Eager loading)> npm install --save @angular/material @angular/cdk
npm WARN @angular/animations@7.1.4 requires a peer of @angular/core@7.1.4 but none is installed. You must install peer dependencies yourself.
npm WARN bootstrap@4.2.1 requires a peer of jquery@1.9.1 - 3 but none is installed. You must install peer dependencies yourself.
npm WARN bootstrap@4.2.1 requires a peer of popper.js@^1.14.6 but none is installed. You must install peer dependencies yourself.
npm WARN @angular/cdk@8.1.1 requires a peer of @angular/core@^8.0.0 || ^9.0.0-0 but none is installed. You must install peer dependencies yourself.
npm WARN @angular/cdk@8.1.1 requires a peer of @angular/common@^8.0.0 || ^9.0.0-0 but none is installed. You must install peer dependencies yourself.
npm WARN @angular/material@8.1.1 requires a peer of @angular/animations@^8.0.0 || ^9.0.0-0 but none is installed. You must install peer dependencies yourself.
npm WARN @angular/material@8.1.1 requires a peer of @angular/core@^8.0.0 || ^9.0.0-0 but none is installed. You must install peer dependencies yourself.
npm WARN @angular/material@8.1.1 requires a peer of @angular/common@^8.0.0 || ^9.0.0-0 but none is installed. You must install peer dependencies yourself.
npm WARN @angular/material@8.1.1 requires a peer of @angular/forms@^8.0.0 || ^9.0.0-0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current:
{"os":"win32","arch":"x64"})

+ @angular/cdk@8.1.1
+ @angular/material@8.1.1
updated 2 packages and audited 40183 packages in 19.079s
found 607 vulnerabilities (2 low, 5 moderate, 600 high)
  run `npm audit fix` to fix them, or `npm audit` for details
PS D:\Angular-DemoApps\Angular 6 - Dynamic tab implementation with Multiple component (Component as a tab, Eager loading)>

After completing the installation we are going to use our previous login and registration pages(pages that we use in all our posts), That will be loaded into tab or will act as a tab.


Following are the login and registration component firstpage and secondpage component respectively.

first-page.component.html
<div class="container">
  <form #frm="ngForm" id="frm"><br><br>
    <div class="row">
      <div class="col-lg-4"></div>
      <div class="col-lg-6">
        <h3>Login</h3>
        <table>
          <thead>
            <th>
              Login Name
            </th>
            <th>
              <input class="form-control" type="text" [(ngModel)]="user.userName" name="userName" required>
            </th>
          </thead>
          <tbody>
            <tr>
              <th>
                Password
              </th>
              <td><input class="form-control" type="password" [(ngModel)]="user.password" name="password"></td>
            </tr>
            <tr>
              <td>
              </td>
              <td>
                <button class="btn btn-primary">Login</button>&nbsp;
                <button class="btn btn-light" (click)="registerMe();">Register Me</button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </form>
</div>

first-page.component.ts
import { Component, OnInit, HostListener, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { User } from '../user/user';
import { HomepageComponent } from '../homepage/homepage.component';

@Component({
  selector: 'app-first-page',
  templateUrl: './first-page.component.html',
  styleUrls: ['./first-page.component.css']
})
export class FirstPageComponent implements OnInit {
  user: User;

  constructor(private homePagereferance: HomepageComponent) { }

  ngOnInit() {
    this.user = new User();
   }

  registerMe() {
    this.homePagereferance.openTab('second page', 'secondpage', undefined);
  }

}



second-page.components.html
<div class="container">
  <div class="row">
    <div class="col-lg-4"></div>
    <div class="col-lg-6">
      <form action="" #frm="ngForm">
        <h3>Registration</h3>
        <table>
          <tr>
            <td>User name</td>
            <td><input [(ngModel)]="user.userName" class="form-control" type="text" name="userName"></td>
          </tr>
          <tr>
            <td>password</td>
            <td><input [(ngModel)]="user.password" class="form-control" type="password" name="password"></td>
          </tr>
          <tr>
            <td>confirm password</td>
            <td><input class="form-control" type="password" name="password"></td>
          </tr>
          <tr>
            <td>email</td>
            <td><input [(ngModel)]="user.email" class="form-control" type="text" name="email"></td>
          </tr>
          <tr>
            <td>contact No</td>
            <td><input [(ngModel)]="user.contactNo" class="form-control" type="text" name="contactNo"></td>
          </tr>
          <tr>
            <td>address</td>
            <td><input [(ngModel)]="user.address" class="form-control" type="text" name="address"></td>
          </tr>
          <tr>
            <td></td>
            <td><button class="btn btn-light">Cancel</button>&nbsp;<button class="btn btn-primary">save</button></td>
          </tr>
        </table>
      </form>
    </div>
  </div>
</div>

second-page.component.ts
import { Component, OnInit, ViewChild } from '@angular/core';
import { User } from '../user/user';

@Component({
  selector: 'app-second-page',
  templateUrl: './second-page.component.html',
  styleUrls: ['./second-page.component.css']
})
export class SecondPageComponent implements OnInit {
  user: User;

  constructor() { }

  ngOnInit() {
    this.user = new User();
  }

}

Above are the components containing simple code to show login and registration page respectively, which will act as a tab.

But these components cannot act as a tab directly. Let’s make them capable to become a tab.
We are going to make a new component which will act as a wrapper to all component who want to become a tab.
In tab-wrapper component we are simply giving the selector of all components that will act as a tab of that will be contained in the tab.
tabId is the variable accepted as input from outside of a component will decide which selector is going to be used means tabId will decide in any particular tab, which component will be shown.

tab-wrapper.component.html
<app-first-page *ngIf="tabId == 'firstpage'"></app-first-page>
<app-second-page *ngIf="tabId == 'secondpage'"></app-second-page>

tab-wrapper.component.ts
import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-tab-wrapper',
  templateUrl: './tab-wrapper.component.html',
  styleUrls: ['./tab-wrapper.component.scss'],
})
export class TabWrapperComponent implements OnInit {

  @Input("tabId") tabId: string;

  constructor() { }

  ngOnInit() { }

}


After creating tab-wrapper component, we are ready to implement our home page where tabs are going to play the game. Let’s create a home page.

  Here we have used the tabs from the material, but material tab doesn’t allow us to handle other things like close the tab, so to achieve that we have divided tab in two-parts, header, and the contents.  
  In the header part, we have used navigation and bound an array of tabs which will show the headers of tabs. In the content part, we have used tab-wrapper component which will act as a wrapper component for all other components to become a tab.  

homepage.component.html  
<div class="container no-gutters">
    <div class="row">
        <div class="col-lg-12">
            <h2>Welcome to dynamic tab with multiple component demo</h2>
        </div>
        <div class="col-lg-12">
            <a class="btn btn-outline-light" (click)="openLogin()">open login
                component in
                tab</a>
            &nbsp;
            <a class="btn btn-outline-light" (click)="openReg()">open
                registeration
                component tab</a>
            &nbsp;
            <a class="btn btn-outline-light" (click)="duplicate()"> Duplicate</a>
        </div>
    </div>
    <div class="row no-gutters">
        <nav mat-tab-nav-bar mainTabs class="scroll mainTabs">
            <a *ngFor="let link of navLinks; let i = index" mat-tab-link class="tab-font" (click)="activate(i);"
                [active]="activeLinkIndex == i" title="{{link.label}}">
                <span>{{link.label}}</span>
                <span (click)="closeTab(i)" class="closeIco">
                    <img src="" alt="X">
                </span>
            </a>
        </nav>
        <mat-tab-group [selectedIndex]="activeLinkIndex" class="col-lg-12">
            <mat-tab *ngFor="let link of navLinks; let i = index" label="{{link.label}}" (click)="activeLinkIndex = i;">
                <app-tab-wrapper [tabId]="link.link"></app-tab-wrapper>
            </mat-tab>
        </mat-tab-group>
    </div>
</div>

To show our designed header we have made original header of mat-tab hidden.

homepage.component.css

/deep/ .mat-tab-header {
    display: none!important;
}

Now real business comes into existence, here we have declared one array which will be used for maintaining tab-list and managed our logic to add a tab, remove the tab and maintain selected tab as well. You can understand it easily by looking at code.

homepage.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-homepage',
  templateUrl: './homepage.component.html',
  styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
  activeLinkIndex = -1;
  navLinks: any[];
  link: any;
  counter: number = 0;

  constructor() { }

  ngOnInit() {
    this.navLinks = [];
  }

  openLogin() {
    let tabTitle = "First Page";
    let tabId = "firstpage";
    let param = undefined;
    this.openTab(tabTitle, tabId, undefined);
  }

  openReg() {
    let tabTitle = "Second Page";
    let tabId = "secondpage";
    let param = undefined;
    this.openTab(tabTitle, tabId, undefined);
  }

  duplicate() {
    this.counter++;
    let tabTitle = "duplicate " + this.counter;
    let tabId = "firstpage";
    let param = undefined;
    this.openTab(tabTitle, tabId, "allow duplicate");
  }

  public openTab(heading: String, route: String, parameter: any) {
    console.log("openTab in layout... IN");
    var res;
    var navFlag = false;
    var tabLength = this.navLinks.length;
    if (parameter == "allow duplicate") {
      navFlag = false;
    } else {
      for (var i = 0; i < this.navLinks.length; i++) {
        if (this.navLinks[i].link == route) {
          navFlag = true;
          if (!this.navLinks[i].parameter) {
            this.activeLinkIndex = i;
          } else {
            this.activeLinkIndex = i;
          }
          break;
        } else {
          navFlag = false;
        }
      }
    }

    if (navFlag == false) {
      this.navLinks.push({
        label: heading,
        link: route,
        index: tabLength,
        parameter: parameter
      });

      if (parameter == "allow duplicate") {
        this.activeLinkIndex = this.navLinks.length;
      } else {
        this.activeLinkIndex = tabLength;
      }
    }
    console.log("openTab in layout... OUT");
  }

  closeTab(index: number) {
    console.log("closeTab in layout component... IN");
    var maxTabIndex;
    var res;
    if (this.activeLinkIndex == index) {
      maxTabIndex = this.navLinks.length - 1;
      if (maxTabIndex == 0) {
        this.navLinks.splice(index, 1);
      } else if (index < maxTabIndex) {
        this.navLinks.splice(index, 1);

        this.activeLinkIndex = index;
      } else if (index == maxTabIndex) {
        this.navLinks.splice(index, 1);
        maxTabIndex = this.navLinks.length - 1;

        this.activeLinkIndex = maxTabIndex;
      } else {
      }
    } else if (this.activeLinkIndex > index) {
      this.navLinks.splice(index, 1);
      this.activeLinkIndex = this.activeLinkIndex - 1;
    } else {
      this.navLinks.splice(index, 1);
    }
    if (this.navLinks.length <= 0) {
    }
    console.log("closeTab in layout component... OUT");
  }

  closeOtherTabs(index: number) {
    console.log("closeOtherTabs in layout component... IN");
    this.link = this.navLinks[index];
    this.navLinks = [];
    this.navLinks.push(this.link);

    this.activeLinkIndex = 0;
    this.link = undefined;
    console.log("closeOtherTabs in layout component... OUT");
  }

  closeAllTabs() {
    console.log("closeAllTabs in layout component... IN");
    this.activeLinkIndex = -1;
    this.navLinks = [];
    console.log("closeAllTabs in layout component... OUT");
  }

  activate(index: number) {
    console.log("activate in layout component... IN");
    if (!this.navLinks[index].parameter) {
      this.activeLinkIndex = index;
    } else {
      this.activeLinkIndex = index;
    }
    console.log("activate in layout component... OUT");
  }

}




And Let's check the output



And that’s it,
we have successfully created dynamic tab logic into our angular project.

Note: this is the logic where we have used selectors to load the tab content, so it is eagerly loaded structure. And the main advantage of this structure is, we don’t need to do anything to preserve the state of each tab content, we don’t need to save the state of each tab.  

Hope you like it.

Here is the link to download the project[download]

Comments