How to Use Dynamic Tabs and How To Maintain State for Dynamic Tabs in Syncfusion Angular?

Description:
I am implementing dynamic tabs in my Angular application and need to maintain their state across different user sessions. I am using Syncfusion components along with Angular’s router.

Currently, I:

  • Fetch tabs from the backend and open them dynamically.
  • Store tab data in localStorage and restore them when the app initializes.
  • Save and retrieve each tab’s state using a tabService.
  • Handle tab activation and scrolling when tabs are added or removed.

Here is my approach:

  • Loading Tabs on Initialization:

    • Fetch tabs from the backend based on userId and open them.
    • Restore tabs from localStorage.
  • Opening and Closing Tabs:

    • Check if a tab exists before adding it.
    • Set the active tab when a new tab is opened.
    • Save the updated list of tabs to localStorage.
  • Maintaining Active Tab State:

    • When a user logs out, I save the open tabs to the backend.
    • When a user logs back in, I fetch and restore them.
    • Ensure that router.navigateByUrl() properly updates the active tab.
  • Handling Sidebar Toggle & Resizing:

    • Adjust the tab container width when the sidebar is toggled.
    • Handle scrolling when the tab list overflows.
  • Issues I am Facing:
    1.Is there a better way to persist the tab state without affecting performance?
    2.How can I store tab state which contains forms with complex grid?
  • Below is my layout.ts code
  • export class LayoutComponent implements OnInit {

      //#region Public Variables
      @ViewChild('tabList') tabList!: ElementRef;
      @ViewChild(HeaderComponent) headerComponent!: HeaderComponent;

      public isSidebarActive: boolean = false;
      public sidebar!: boolean;
      public tabs: ITab[] = [];
      public userId: any;
      public showScrollButtons: boolean = false;
      public bsModalRef!: BsModalRef;
      public screenWidth: any;
      public containerWidth: any;
      public screenHeight: any;
      //#endregion

      constructor(
        private activedRoute: ActivatedRoute,
        private router: Router,
        private tabService: TabService,
        private changeDetectorRef: ChangeDetectorRef,
        private toasterService: ToastrService,
      ) {
        if (this.activedRoute.snapshot.queryParams['sidebar'] === "false") {
          this.sidebar = false;
          localStorage.setItem('sidebar', 'false')
        }
        else {
          localStorage.setItem('sidebar', 'true')
          this.sidebar = true;
        }
      }


      ngOnInit(): void {
        this.screenWidth = window.innerWidth;
        this.screenHeight = window.innerHeight;
        this.setTabContainerWidth();
        this.loadTabsFromLocalStorage();
        this.userId = localStorage.getItem('userId');
        this.getTabList(this.userId);
        this.tabService.openTab$.subscribe(tab => {
          this.openTab(tab.route, tab.label, tab.isActive);
        });
      }

      public setTabContainerWidth(): void {
        const sidebarWidth = this.isSidebarActive ? 74 : 300;
        const newWidth = this.screenWidth - sidebarWidth;
        this.containerWidth = newWidth;
        this.changeDetectorRef.detectChanges();
        this.checkScrollButtons();
      }

      public toggleSidebar(): void {
        this.isSidebarActive = !this.isSidebarActive;
        this.setTabContainerWidth();
      }

      public getTabList(userId: number): void {
        this.tabService.getTabMenuByUserId(userId)
          .subscribe({
            next: (response: any) => {
              // Iterate over the array of tabs received from the backend
              response.forEach((tab: { route: string; label: string; isActive: any; }) => {
                // Add each tab to the tabs array
                this.openTab(tab.route, tab.label, tab.isActive);
              });
            },
            error: (error: any) => {
              showErrorMessage('', 'Error while get lab list!', this.toasterService);
            },
            complete: () => { }
          });
      }

      public openTab(route: string, label: string, isActive: boolean): void {
        const existingTab = this.tabs.find(tab => tab.route === route);

        if (!existingTab) {
          this.tabs.push({ route, label, isActive, userId: this.userId });

          // Restore tab state if it exists
          const savedState = this.tabService.getTabState(route);
          if (savedState) {
          }
        } else {
          existingTab.label = label;
          existingTab.isActive = isActive;
        }

        if (isActive) {
          this.setActiveTab(route);
        }

        this.changeDetectorRef.detectChanges();
        this.checkScrollButtons();
      }

      public closeTab(index: number): void {
        const tab = this.tabs[index];
        this.tabService.removeTabState(tab.route); // Remove saved state

        this.tabs.splice(index, 1);
        this.saveTabsToLocalStorage();

        if (this.tabs.length > 0) {
          const nextTab = this.tabs[index] || this.tabs[index - 1];
          this.router.navigateByUrl(nextTab.route);
          this.setActiveTab(nextTab.route);
        } else {
          this.router.navigateByUrl('/main');
        }

        this.changeDetectorRef.detectChanges();
        this.checkScrollButtons();
      }

      public loadTabsFromLocalStorage(): void {
        const storedTabs = localStorage.getItem('tabs');
        if (storedTabs) {
          this.tabs = JSON.parse(storedTabs);
        }
      }

      public saveTabsToLocalStorage(): void {
        localStorage.setItem('tabs', JSON.stringify(this.tabs));
      }

      public setActiveTab(route: string): void {
        this.tabs.forEach(tab => tab.isActive = false);
        const activeTab = this.tabs.find(tab => tab.route === route);
        if (activeTab) {
          if (activeTab.label == "Search Fuel Jobs") {
            const urlWithParams = this.router.createUrlTree([route], {
              queryParams: { queryParam: 1 }
            });
            this.router.navigateByUrl(urlWithParams);
          }
          else {
            this.router.navigateByUrl(route);
          }
          activeTab.isActive = true;
        }
        this.saveTabsToLocalStorage();
        this.changeDetectorRef.detectChanges();
      }

      public handleLogout(): void {
        this.saveTabList();
      }

      public saveTabList(): void {
        this.tabService.addTabs(this.userId, this.tabs)
          .subscribe({
            next: () => { },
            error: (error: any) => {
              showErrorMessage(error.error, "Error While Loading Tabs", this.toasterService);
            },
            complete: () => { }
          });
      }

      //#region Dom Manipulation
      public ngAfterViewInit(): void {
        this.checkScrollButtons();
        this.headerComponent.logoutEvent.subscribe(() => this.handleLogout());
        window.addEventListener('resize', () => {
          this.setTabContainerWidth();
        });
        // Manually trigger change detection
        this.changeDetectorRef.detectChanges();
      }

      @HostListener('window:resize', ['$event'])
      public onResize(): void {
        this.updateScreenDimensions();
        this.setTabContainerWidth();
      }

      private updateScreenDimensions(): void {
        this.screenWidth = window.innerWidth;
        this.screenHeight = window.innerHeight;
      }
      //#endregion

      //#region ScrollTabs
      public scrollLeft(): void {
        const tabListElement = this.tabList.nativeElement;
        tabListElement.scrollLeft -= 200;
        this.changeDetectorRef.detectChanges();
      }

      public scrollRight(): void {
        const tabListElement = this.tabList.nativeElement;
        tabListElement.scrollLeft += 200;
        this.changeDetectorRef.detectChanges();
      }

      public checkScrollButtons(): void {
        const tabListElement = this.tabList.nativeElement;
        this.showScrollButtons = tabListElement.scrollWidth > tabListElement.clientWidth;
        this.changeDetectorRef.detectChanges();
      }

      public scrollToLastTab(): void {
        setTimeout(() => {
          const tabListElement = this.tabList.nativeElement;
          tabListElement.scrollLeft = tabListElement.scrollWidth;
          this.changeDetectorRef.detectChanges();
        }, 0);
      }
      //#endregion

      public closeAllTabs(): void {
        this.tabs.splice(0, this.tabs.length);
        this.saveTabsToLocalStorage();
        this.tabService.clearAllTabStates();
        this.router.navigateByUrl('/main');
      }
    }
  • Below is my layout.html code
  • <div class="content-outer">
        <header (toggleSidebarEvent)="toggleSidebar()" class="header-container d-none"></header>
        <div class="content-wrapper-outer">
          <div [ngStyle]="{ 'width.px': containerWidth }" class="top-list-tab-container">
            <button class="scroll-button left" *ngIf="showScrollButtons" (click)="scrollLeft()">&lt;</button>
            <div class="tab-list-wrapper top-list-tab" #tabList (scroll)="checkScrollButtons()">

              <ul class="nav nav-tabs">
                <li class="nav-item" *ngFor="let tab of tabs; let i = index">
                  <div class="nav-link" [ngClass]="{'active': tab.isActive}" [routerLink]="[tab.route]"
                    (click)="setActiveTab(tab.route)"> {{ tab.label }}
                    <button type="button" aria-label="Close" (click)="closeTab(i)">
                      <svg (click)="closeTab(i)" width="10" height="8" viewBox="0 0 8 8" fill="none"
                        xmlns="http://www.w3.org/2000/svg">
                        <path
                          d="M1.55235 0.464466C1.25149 0.163606 0.765349 0.163606 0.464489 0.464466C0.163629 0.765326 0.163629 1.25146 0.464489 1.55232L2.91217 4L0.464489 6.44768C0.163629 6.74854 0.163629 7.23467 0.464489 7.53553C0.765349 7.83639 1.25149 7.83639 1.55235 7.53553L4.00002 5.08786L6.4477 7.53553C6.74856 7.83639 7.2347 7.83639 7.53556 7.53553C7.83642 7.23467 7.83642 6.74854 7.53556 6.44768L5.08788 4L7.53556 1.55232C7.83642 1.25146 7.83642 0.765326 7.53556 0.464466C7.2347 0.163606 6.74856 0.163606 6.4477 0.464466L4.00002 2.91214L1.55235 0.464466Z"
                          fill="#5D6B9A" />
                      </svg>
                    </button>
                  </div>
                </li>
              </ul>

            </div>
            <button class="scroll-button right" *ngIf="showScrollButtons" (click)="scrollRight()">&gt;</button>
            <button *ngIf="tabs.length!=0" aria-label="Close" class="close-all-tabs" (click)="closeAllTabs()">
              <svg (click)="closeAllTabs()" width="10" height="8" viewBox="0 0 8 8" fill="none"
                xmlns="http://www.w3.org/2000/svg">
                <path
                  d="M1.55235 0.464466C1.25149 0.163606 0.765349 0.163606 0.464489 0.464466C0.163629 0.765326 0.163629 1.25146 0.464489 1.55232L2.91217 4L0.464489 6.44768C0.163629 6.74854 0.163629 7.23467 0.464489 7.53553C0.765349 7.83639 1.25149 7.83639 1.55235 7.53553L4.00002 5.08786L6.4477 7.53553C6.74856 7.83639 7.2347 7.83639 7.53556 7.53553C7.83642 7.23467 7.83642 6.74854 7.53556 6.44768L5.08788 4L7.53556 1.55232C7.83642 1.25146 7.83642 0.765326 7.53556 0.464466C7.2347 0.163606 6.74856 0.163606 6.4477 0.464466L4.00002 2.91214L1.55235 0.464466Z"
                  fill="#5D6B9A" />
              </svg>
              <span>Close All Tabs</span>
            </button>
          </div>
          <router-outlet></router-outlet>
        </div>
      </div>


1 Reply

SR Swathi Ravi Syncfusion Team February 11, 2025 12:18 PM UTC

Hi Andrew Morgan,

 

Thank you for reaching out to us and providing detailed information about your implementation. We appreciate your efforts in leveraging Syncfusion components for your Angular application.

 

To address your query, the Tab component in Syncfusion is primarily a layout component, and its state persistence can be managed using the enablePersistence property. This property ensures that the selected tab index is maintained across page reloads. 

 

https://ej2.syncfusion.com/angular/documentation/tab/how-to/set-state-persistence-of-the-tab-component

 

However, for the content within the tabs (such as forms or grids), you will need to handle state management at the application level. If you are using the Grid component as tab content, you can leverage its state persistence feature to maintain the grid’s state (such as sorting, filtering, and column settings). Refer to this https://ej2.syncfusion.com/angular/documentation/grid/state-management#restore-to-previous-state

 

Additional reference, https://stackoverflow.com/questions/58889376/angular-architecture-how-to-handle-state-persistence-with-multiple-http-service

 

Regards,

Swathi


Loader.
Up arrow icon