Creating an Angular Dashboard Layout - Theming


In the previous post we put together our dashboard layout component containing our bare layout frame. Now we are going to be putting together a couple components to use with our framing. Hopefully by the end of this post, you’ll have a good idea of how we can swap out different styled navigation bars and side panels.

A full screen demo of what we’re going to build

 

Prerequisites

 

Create a Shared Module

Before we start creating our components, we are going to create a shared module. The shared module will contain all component that we could potentially use in multiple modules in our project. We can generate our module with the Angluar CLI…

ng generate module shared

And import our new module into our root app module…

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    CoreModule,
    SharedModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { 

 

Creating Our Navigation Bar Component

We will start by creating a navigation bar component. Our component is pretty simple in design with just a single menu button. The majority of our work is in the component class file.

Lets generate our new navigation bar component…

ng generate component shared/components/navigation-bar

Our simple HTML structure…

<div class="navigation-top-bar">
  <div class="toggler" role="button">
    <svg viewBox="0 0 100 80" width="32" height="32">
      <rect width="100" height="18"></rect>
      <rect y="28" width="100" height="18"></rect>
      <rect y="56" width="100" height="18"></rect>
    </svg>
  </div>
  <div class="navigation-extended">
    <ng-content select="[panel=navigationExtended"></ng-content>
  </div>
</div>

And some simple CSS styling…

$background: #F6F6F8;
$background-nav-top: #FFFFFF;
$nav-panel-hover: #38495F;
$nav-panel-active: darken(#38495F, 2%);

.navigation-top-bar {
    background: $background-nav-top;
    width: 100%;
    height: 75px;
    line-height: 75px;
    border-bottom: 1px solid darken($background, 5%);

    .toggler {
        display: flex;
        flex-direction: column;
        height: 100%;
        justify-content: center;
        align-items: center;
        width: 75px;
        border-right: 1px solid darken($background, 5%);
        text-align: center;

        svg {
            fill: darken($background, 10%);
            cursor: pointer;
        }
    }
}

Our navigation bar will look something like this…

Navigation bar screenshot

Before we wire up our menu button to manipulate our side panel frame, I thought it might be neat to incorporate a double click event allowing us to use all three side panel states with the one button. Essentially a single click would toggle between open/collapse and a double click would close the side panel completely.

 

Creating Our Click Directive.

To my knowledge, Angular doesn’t have a binding for double clicks. Fortunately this functionality isn’t too hard to create ourselves. We can accomplish this with an Angular directive. We will created a single/double click directive that will allow us to determine when a button is either single clicked or double clicked.

We will apply this directive to our navigations bar’s menu button which will allow us to toggle between the three side panel state. For a single click the side panel will collapse and open. For a double click the side panel will close completely and open.

First we need to generate our click directive…

ng generate directive shared/directives/single-double-click

The logic is pretty easy to accomplish with a setTimeout. We’ll have two event emitters, one to emit a single click event and another for the double click. We will also have a @HostListener() that listens for click events on the element the directive is applied to. We’ll use a setTimeout with a 250ms delay to allow enough time for a second click to register.

The logic for a directive…

import { Directive, HostListener, Output, EventEmitter } from '@angular/core';

@Directive({
  selector: '[singleDoubleClick]' // I removed the app prefix for our directive
})
export class SingleDoubleClickDirective {

  @Output()
  public onSingleClick: EventEmitter<void>;

  @Output()
  public onDoubleClick: EventEmitter<void>;

  private _singleClickTimeout;
  private _isDoubleClick: boolean;

  constructor() {
    this._isDoubleClick = false;
    this.onSingleClick = new EventEmitter<void>();
    this.onDoubleClick = new EventEmitter<void>();
  }

  @HostListener('click', ['$event'])
  private processClick(event) {
    event.stopPropagation();
    if (this._isDoubleClick === true) {
      clearTimeout(this._singleClickTimeout);
      this._isDoubleClick = false;
      this.onDoubleClick.next()
    } else {
      this._isDoubleClick = true;
      this._singleClickTimeout = setTimeout(() => {
        this.onSingleClick.next();
        this._isDoubleClick = false;
      }, 250);
    }
  }
}

Update our shared module by exporting our directive…

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NavigationBarComponent } from './components/navigation-bar/navigation-bar.component';
import { SingleDoubleClickDirective } from './directives/single-double-click.directive';

@NgModule({
  declarations: [
    NavigationBarComponent, 
    SingleDoubleClickDirective
  ],
  imports: [
    CommonModule
  ],
  exports: [
    NavigationBarComponent, 
    SingleDoubleClickDirective
  ]
})component
export class SharedModule { }

Now we can apply our directive to our navigation bar menu button…

<div class="navigation-top-bar">
  <div class="toggler" role="button">
    <svg singleDoubleClick
        (onSingleClick)="handleSingleClick()"
        (onDoubleClick)="handleDoubleClick()"
        viewBox="0 0 100 80" width="32" height="32">
      <rect width="100" height="18"></rect>
      <rect y="28" width="100" height="18"></rect>
      <rect y="56" width="100" height="18"></rect>
    </svg>
  </div>
  <div class="navigation-extended">
    <ng-content select="[panel=navigationExtended"></ng-content>
  </div>
</div>

And update our navigation bar component class…

import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { SidePanelService } from '../../../core/';
import { SidePanelState } from '../../../core/';

@Component({
  selector: 'app-navigation-bar',
  templateUrl: './navigation-bar.component.html',
  styleUrls: ['./navigation-bar.component.scss']
})
export class NavigationBarComponent implements OnInit {
  private _subscriptionsSubject$: Subject<void>;
  public currentPanelState: SidePanelState;

  constructor(private _sidePanelService: SidePanelService) {
    this._subscriptionsSubject$ = new Subject<void>();
  }

  ngOnInit(): void {
    this._sidePanelService
      .panelStateChanges
      .pipe(takeUntil(this._subscriptionsSubject$))
      .subscribe((state: SidePanelState) => this.currentPanelState = state);
  }

  public handleSingleClick(): void {
    if (this.currentPanelState === SidePanelState.CLOSE || this.currentPanelState === SidePanelState.COLLAPSE) {
      this._sidePanelService.changeState(SidePanelState.OPEN);
    } else { 
      this._sidePanelService.changeState(SidePanelState.COLLAPSE);
    }
  }

  public handleDoubleClick(): void {
    if (this.currentPanelState === SidePanelState.CLOSE) {
      this._sidePanelService.changeState(SidePanelState.OPEN)
    } else {
      this._sidePanelService.changeState(SidePanelState.CLOSE);
    }
  }

  ngOnDestroy(): void {
    this._subscriptionsSubject$.next();
    this._subscriptionsSubject$.complete();
  }
}

The component class makes use of our SidePanelService by updating the state of the side panel based on single and double click events.

 

Creating Our Side Panel

And finally our side panel component. We’re going to make the links of our side panel dynamic by passing an array of links to the component using the @Input decorator.

First lets generate a new component…

ng generate component shared/components/navigation-side-panel

And update our shared module, exporting our new component…

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { NavigationBarComponent } from './components/navigation-bar/navigation-bar.component';
import { NavigationSidePanelComponent } from './components/navigation-side-panel/navigation-side-panel.component';
import { SingleDoubleClickDirective } from './directives/single-double-click.directive';

@NgModule({
  declarations: [
    NavigationBarComponent, 
    NavigationSidePanelComponent, 
    SingleDoubleClickDirective
  ],
  imports: [
    CommonModule,
    RouterModule
  ],
  exports: [
    NavigationBarComponent, 
    NavigationSidePanelComponent, 
    SingleDoubleClickDirective
  ]
})
export class SharedModule { }

Our side panel component HTML template…

<div class="navigation-panel">
  <div class="navigation-panel-brand">L<span [ngClass]="currentPanelState">ogo</span></div>
  <ul class="navigation-panel-menu" *ngIf="links && links.length > 0">
    <li *ngFor="let link of links">
      <a [routerLink]="link.url" 
          [routerLinkActive]="['active']"
          [routerLinkActiveOptions]="{exact: true}">
        <i class="fas fa-home" [ngClass]="link.iconClass"></i>
        <span [ngClass]="currentPanelState">{{ link.text }}</span>
      </a>
    </li>
  </ul>
</div>

Our styling for our side panel…

$nav-panel-background: #3B4D63;
$nav-panel-hover: #38495F;
$nav-panel-active: darken(#38495F, 2%);
$link-unvisted: #82ff80;
$text-color-brand: #FFFFFF;

.navigation-panel {
    background: $nav-panel-background;
    overflow: hidden;
    height: 100vh;

    .navigation-panel-brand {
        height: 75px;
        line-height: 75px;
        padding-left: 2rem;
        font-size: 1.5rem;
        color: $text-color-brand;
        font-weight: 700;
    
        span {
            transition: opacity 0.2s;
            
            &.open {
                opacity: 1;
            }
            &.close {
                opacity: 0;
            }
            &.collapse {
                opacity: 0;
            }
        }
    }

    .navigation-panel-brand,
    .navigation-panel-menu > li {
        border-bottom: 1px solid $nav-panel-active;
    }

    .navigation-panel-menu {
        li {
            a {
                display: block;
                width: 100%;
                height: 100%;
                line-height: 4rem;
                cursor: pointer;
                padding-left: 1.8rem;
                text-transform: uppercase;
                font-weight: 700;
                white-space: nowrap;
                transition: background 0.2s;

                i {
                margin-right: 0.5rem;
                }

                span {
                    transition: opacity 0.2s;

                    &.open { opacity: 1; }
                    &.close { opacity: 0; }
                    &.collapse { opacity: 0; }
                }
            }
            &:hover {
                background: $nav-panel-hover;
            }

            .active {
                background: $nav-panel-active;
            }
        }
    }
}

And our component class…

import { Component, OnInit, OnDestroy, Input } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { SidePanelService, SidePanelState } from '../../../core';
import { NavigationLink } from './navigation-link.model';

@Component({
  selector: 'app-navigation-side-panel',
  templateUrl: './navigation-side-panel.component.html',
  styleUrls: ['./navigation-side-panel.component.scss']
})
export class NavigationSidePanelComponent implements OnInit, OnDestroy {
  @Input()
  public links: NavigationLink[];

  private _subscriptionsSubject$: Subject<void>;
  public currentPanelState: SidePanelState;
  public SidePanelState = SidePanelState;

  constructor(private _sidePanelService: SidePanelService) {
    this._subscriptionsSubject$ = new Subject<void>();
  }

  ngOnInit(): void {
    this._sidePanelService.panelStateChanges
      .pipe(takeUntil(this._subscriptionsSubject$))
      .subscribe((state: SidePanelState) => this.currentPanelState = state);
  }

  ngOnDestroy(): void {
    this._subscriptionsSubject$.next();
    this._subscriptionsSubject$.complete();
  }
}

We still need to create a NavgationLink class to model our side panel links.

touch src/app/shared/components/navigation-side-panel/navigation-link.model.ts

Our navigation link class will look like this…

export class NavigationLink {
  constructor(
    public text: string, // Text to display
    public url: string | string[], // The routerLink string or string[]
    public iconClass: string // And a css class for an icon, since we're using font awesome.
  ) {}
}

By creating this class and passing an collection of NavigationLink as input, we’ll be able to expand on the list easily without much modification.

 

Using Our Components

Now that we have our components, we can integarte them into our dashboard layout. Lets update our root app component…

<app-dashboard-layout [configuration]="configuration">
  <div container="sidePanel">
    <app-navigation-side-panel [links]="links"></app-navigation-side-panel>
  </div>
  <div container="navigationBar">
    <app-navigation-bar></app-navigation-bar>
  </div>
  <div container="mainContent">
    <router-outlet></router-outlet>
  </div>
</app-dashboard-layout>

Our app component class…

import { Component } from '@angular/core';
import { SidePanelState, DashboardLayoutConfiguration, SidePanelPosition } from './core';
import { NavigationLink } from './shared';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  public configuration: DashboardLayoutConfiguration;
  public links: NavigationLink[];
  
  constructor() {
    this.configuration = new DashboardLayoutConfiguration(
      SidePanelPosition.LEFT, 
      SidePanelState.OPEN
    );
    this.createLinks();
  }

  private createLinks() {
    this.links = [
      new NavigationLink("Home", ['home'], "fas fa-home"),
      new NavigationLink("Dashboard", ['dashbaord'], "fas fa-tachometer-alt"),
      new NavigationLink("Account Info", ['account'], "fas fa-user-circle")
    ]
  }
}

Update our app routing file…

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { HomeComponent } from './pages/home/home.component';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent   
  },
  {
    path: 'dashboard',
    component: DashboardComponent)
  },
  {
    path: '**',
    redirectTo: 'dashboard',
    pathMatch: 'full'
  }
];

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

 

And finally generate our two components page components.

ng generate component src/app/pages/dashboard
ng generate component src/app/pages/home

Stackblitz Result

Now we have a fully structured and themed dashboard layout. Below I’ve embeded a working Stackblitz of our layout and components. Our menu button will handle all three states of our side panel, single clicks to open/collapse and double clicks to close/open.

 

The completed github repository can be found here

 

 


comments powered by Disqus