Dynamic tabs in angular (Lazy loading) with Routing


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 lazy-loaded component, which means all components that will act as a tab will be loaded in lazily with help of routing. Let’s do it step by step.
If you are not interested in lazy loading and you want tabs with simple selector please click here to visit post tabs with eager loading 

[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.
The only difference is we are going to add module and routing module to each component to make them load lazily.

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">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();
   }

}


first-page-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { FirstPageComponent } from './first-page.component';

const routes: Routes = [
  { path: '', component: FirstPageComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class FirstPageRoutingModule { }

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();
  }

}

second-page-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SecondPageComponent } from './second-page.component';

const routes: Routes = [
  { path: '', component: SecondPageComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class SecondPageRoutingModule { }

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 use routing to show/load components, here we have a parent(homepage) component which will be loaded at very first. Then we are going to use child routing, that loads the component as a child of parent(homepage) component.
But where are the tabs…, So we are going to maintain the navbar which will act as a tab and on click of each navigation, child component will be loaded into router-outlet.

We have used two router-outlet and hence two main routing files
1         App-routing to load home page
2         Homepage-routing where another component will be loaded as a child

It will be very clear when we will look at the code.

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>&nbsp;&nbsp;
                <span (click)="closeTab(i)">
                    <i>X</i>
                </span>
            </a>
        </nav>
    </div>
    <div class="row no-gutters">
        <div class="col-lg-12">
            <router-outlet></router-outlet>
        </div>
    </div>
</div>

As discussed, we have used a navigation bar to show the tab list and added title and close button by ourselves, below that there is router-outlet to load the content of each tab means load particular component belonging to that route.
Now let’s check real business, here we have declared one array which will be used for maintaining tab-list and managed our logic to add a tab, remove tab and maintain the selected tab as well. Here on click of a tab, we are going to load the component by using navigation method and, the list of components to load and their paths are configured into homepage-routing file.
 You can understand it easily by looking at code.

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

@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(private router: Router) { }

  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 = "allow duplicate";
    this.openTab(tabTitle, tabId, param);
  }

  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 != undefined && 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 || parameter == "allow duplicate") {
            this.router.navigate([this.navLinks[i].link]).then(nav => {
              if (nav) {
                this.activeLinkIndex = i;
              }
            });
            this.activeLinkIndex = i;
          } else {
            this.router
              .navigate([
                this.navLinks[i].link,
                this.navLinks[i].parameter
              ])
              .then(nav => {
                if (nav) {
                  this.activeLinkIndex = i;
                }
              });
            this.activeLinkIndex = i;
          }
          break;
        } else {
          navFlag = false;
        }
      }
    }

    if (navFlag == false) {
      this.navLinks.push({
        label: heading,
        link: route,
        index: tabLength,
        parameter: parameter
      });
      var i = this.navLinks.length - 1;
      if (!this.navLinks[i].parameter || parameter == "allow duplicate") {
        this.router
          .navigate([this.navLinks[tabLength].link])
          .then(nav => {
            navFlag = true;
            if (nav) {
              this.activeLinkIndex = tabLength;
            }
          });
        this.activeLinkIndex = tabLength;
      } else {
        this.router
          .navigate([
            this.navLinks[tabLength].link,
            this.navLinks[i].parameter
          ])
          .then(nav => {
            navFlag = true;
            if (nav) {
              this.activeLinkIndex = tabLength;
            }
          });
        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);
        res = this.router.navigate(["/"]);
      } else if (index < maxTabIndex) {
        this.navLinks.splice(index, 1);
        res = this.router
          .navigate([this.navLinks[index].link])
          .then(nav => {
            if (nav) {
              this.activeLinkIndex = index;
            }
          });
        this.activeLinkIndex = index;
      } else if (index == maxTabIndex) {
        this.navLinks.splice(index, 1);
        maxTabIndex = this.navLinks.length - 1;
        res = this.router
          .navigate([this.navLinks[maxTabIndex].link])
          .then(nav => {
            if (nav) {
              this.activeLinkIndex = maxTabIndex;
            }
          });
        this.activeLinkIndex = maxTabIndex;
      } else {
        res = this.router.navigate(["/"]);
      }
    } 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) {
      this.router.navigate(['homePage'])
    }
    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.router.navigate([this.navLinks[0].link]).then(nav => {
      if (nav) {
        this.activeLinkIndex = 0;
      }
    });
    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.router.navigate(["/"]);
    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.router.navigate([this.navLinks[index].link]).then(nav => {
        if (nav) {
          this.activeLinkIndex = index;
        }
      });
      this.activeLinkIndex = index;
    } else {
      this.router
        .navigate([
          this.navLinks[index].link,
          this.navLinks[index].parameter
        ])
        .then(nav => {
          if (nav) {
            this.activeLinkIndex = index;
          }
        });
      this.activeLinkIndex = index;
    }
    console.log("activate in layout component... OUT");
  }

}


Homepage-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomepageComponent } from './homepage.component';

const routes: Routes = [
  {
    path: '', component: HomepageComponent, children:
      [
        { path: 'firstpage', loadChildren: '../first-page/first-page.module#FirstPageModule' },
        { path: 'secondpage', loadChildren: '../second-page/second-page.module#SecondPageModule' }
      ]
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class HomepageRoutingModule { }

Here is 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 routing to load the components, so it will not save/maintain the state of tab. You have to made some provision while switching the tab, either you can ask user to save the data before switching the tab or you can save the state of tab in to the local storage and restore when come back. 

Hope you like it.

Here is the link to download the project

if you want to use eagerly loaded tabs and maintained state please check our prev post here -tab with maintained state 

Comments