Creating an Angular Dashboard Layout - Theming
Published on
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.
Prerequisites
- Install Node
- Install Angular CLI
- Creating an Angular Dashboard Layout - Framing
- OR Clone this github repository
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…
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
More Posts
- Creating an Angular Dashboard Layout - Framing
- Creating an Angular Dashboard Layout - Theme
- Creating an Angular Dashboard Layout - Content Grid