Creating an Angular Component Library - Overlay Side Panel Component


Another useful component that you might commonly see is a slide out side panel. Right now at my current position we’re using Monday for project/task management and communication. If you’ve ever used Monday, clicking on a task item will slide out a side panel from the right where all communication for that task is displayed. This has inpsired me to recreate this feature as an reusable Angular component.

A full screen demo of what we’re going to build.
The Show More buttons will toggle the side panel.

 

Prerequisites

 

Generate Our Overlay Side Panel

For our component we will need a module, a component, a service and an enum type. First let generate the items we need.

ng generate module components/overlay-side-panel --project=foo
ng generate component components/overlay-side-panel --project=foo
ng generate service components/overlay-side-panel/overlay-side-panel --project=foo

We will need to update our module file to export our component and provide our service to the importing module.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { OverlaySidePanelService } from './overlay-side-panel.service';
import { OverlaySidePanelComponent } from './overlay-side-panel.component';

@NgModule({
  declarations: [
    OverlaySidePanelComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    OverlaySidePanelComponent
  ],
  providers: [
    OverlaySidePanelService
  ]
})
export class OverlaySidePanelModule { 

We will also need to create an enum for our overlay side panel style.

touch projects/foo/src/lib/components/overlay-side-panel/overlay-side-panel-style.enum.ts

And our enum values

export enum OverlaySidePanelStyle {
  TRANSPARENT = 'overlay-style-transparent',
  DIM_DARK = 'overlay-style-dim-dark',
  DIM_LIGHT = 'overlay-style-dim-light',
}

Now we can4 update our components directory’s public_api.ts file to export our module, component, service, and enum.

// Overlay Side Panel
export * from './overlay-side-panel/overlay-side-panel.component';
export * from './overlay-side-panel/overlay-side-panel.module';
export * from './overlay-side-panel/overlay-side-panel.service';
export * from './overlay-side-panel/overlay-side-panel-style.enum';

 

Creating Our Service

In our past components we made heavy use of the <ng-content> element to create dynamic content. For this component we going to make the content programatically dynamic. What do I mean by this? When using <ng-content>, your content is more or less paritally dynamic, meaning you have the declare the content in the template. This approach doesn’t allow us to change the content on the fly.

If you have ever use the Material Design modal component, you will know that you create the content of the modal by passing a component type to a function as a parameter. From the component type, a new component is created and added to the DOM.

This is the approach we are going to take for this component. Lets first setup our service which will allow us to set the content of the side panel along with showing/hiding the panel.

import { Injectable, Type } from '@angular/core';
import { BehaviorSubject, Subject, Observable } from 'rxjs';

@Injectable()
export class OverlaySidePanelService {
  private _isPanelVisible: boolean;
  private _closePanelSource: BehaviorSubject<boolean>;

  private _contentChangeSource: Subject<Type<any>>;

  constructor() {
    this._isPanelVisible = false;
    this._closePanelSource = new BehaviorSubject<boolean>(this._isPanelVisible);
    this._contentChangeSource = new Subject<any>();
  }

  public onPanelVibilityChange(): Observable<boolean> {
    return this._closePanelSource.asObservable();
  }

  public onContentChange(): Observable<Type<any>> {
    return this._contentChangeSource.asObservable();
  }

  public setContent(content: Type<any>): void {
    this._contentChangeSource.next(content);
  }

  public show(): void {
    this._isPanelVisible = true;
    this._closePanelSource.next(this._isPanelVisible);
  }

  public close(): void {
    this._isPanelVisible = false;
    this._closePanelSource.next(this._isPanelVisible);
  }
}

In our service above, our setContent method takes a Type<any>. Angular provides us with a generic type, Type<T> which allows us to pass a component type as a parameter to a function. When we want to change the content of our side panel, we will call this method passing in the component type we want to render in the side panel.

One thing to note is the change to the @Injectable() decorator where I removed the providedIn attribute. By default this is set to root, but for our component we going to provide a new instance of each service for each importing module. This will allow us to have independant side panels for each of our pages.

 

Creating Our Component

Our component is fairly simple. Its made up of a container element containing elements for our overlay and our side panel. We placed an <ng-container> element inside our side panel container which will be replace with our dynamically loaded component.

<div class="overlay-side-panel" [class.collapsed]="!isPanelVisible">
  <div class="overlay" [ngClass]="overlayStyle" 
      *ngIf="isPanelVisible" [@fadeInOut] (click)="close()">
  </div>
  <div class="side-panel">
    <div class="close">
      <span (click)="close()">&times;</span>
    </div>
    <div class="content">
      <ng-container #content></ng-container>
    </div>
  </div>
</div>

Our SCSS styling for our component…

@import '../_shared/scss/variables';
@import '../_shared/scss/mixins';

.overlay-side-panel {
  .overlay {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 100;

    &.overlay-style-transparent {
      background: transparent;
    }

    &.overlay-style-dim-dark {
      background: rgba(#000, 0.3);
    }

    &.overlay-style-dim-light {
      background: rgba(#FFF, 0.5);
    }
  }
  
  .side-panel {
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    width: 450px;
    margin-right: 0;
    background: $card-background;
    overflow-y: auto;
    transition: margin-right 0.3s;
    z-index: 101;
    @include box-shadow;

    .close,
    .content {
      padding: 1.25rem;
    }  

    .close {
      background: $card-background;
      position: sticky;
      top: 0;
      z-index: 2;
      line-height: 1rem;
      font-size: 2rem;
      font-weight: bold;
      
      span {
        cursor: pointer;
        color: #888;
        &:hover {
          color: #292929;
        }
      }
    }
  }

  &.collapsed {
    .side-panel {
      margin-right: -450px;
    }
  }
}

@media screen and (max-width: 450px) {
  .overlay-side-panel {
    .side-panel {
      width: 100%;
    }
  }
}

And our component class file…

import { 
  Component, OnInit, OnDestroy, Input, Type, ComponentFactoryResolver, 
  ViewChild, ViewContainerRef, ComponentFactory } from '@angular/core';
import { Subject, pipe } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { OverlaySidePanelService } from './overlay-side-panel.service';
import { OverlaySidePanelStyle } from './overlay-side-panel-style.enum';
import { FADE_IN_OUT } from '../_shared/animations/fade-in-out.animation';

@Component({
  selector: 'foo-overlay-side-panel',
  templateUrl: './overlay-side-panel.component.html',
  styleUrls: ['./overlay-side-panel.component.scss'],
  animations: [FADE_IN_OUT]
})
export class OverlaySidePanelComponent implements OnInit, OnDestroy {
  @ViewChild('content', { read: ViewContainerRef }) 
  public panelContentRef: ViewContainerRef;

  @Input()
  public overlayStyle: OverlaySidePanelStyle

  public isPanelVisible: boolean;

  private _sidePanelServiceSubject$: Subject<void>;

  constructor(
    private _componentFactoryResolver: ComponentFactoryResolver, 
    private _overlaySidePanelService: OverlaySidePanelService
  ) { 
    this._sidePanelServiceSubject$ = new Subject<void>();
    this.overlayStyle = OverlaySidePanelStyle.DIM_DARK;
  }

  ngOnInit(): void {
    this._overlaySidePanelService.onPanelVibilityChange()
      .pipe(takeUntil(this._sidePanelServiceSubject$))
      .subscribe((visible: boolean) => this.isPanelVisible = visible);

    this._overlaySidePanelService.onContentChange()
      .pipe(takeUntil(this._sidePanelServiceSubject$))
      .subscribe((component: Type<any>) => this._setPanelContent(component));
  }

  public close(): void {
    this._overlaySidePanelService.close();
  }

  private _setPanelContent(component: Type<any>) {
    const componentFactory: ComponentFactory<any> = this._componentFactoryResolver.resolveComponentFactory(component);
    this.panelContentRef.clear();
    this.panelContentRef.createComponent(componentFactory);
  }

  ngOnDestroy() {
    this._sidePanelServiceSubject$.next();
    this._sidePanelServiceSubject$.complete();
  }
}

Our component class will subscribe to the vibility changes and the content changes made with our service. At the top of the class we pull in a reference to our <ng-container> element as a ViewContainerRef. We will use this reference to dynamically load our component. We also inject an instance of ComponentFactoryResolver in our constructor as well.

In _setPanelContent we use our the ComponentFacotryResolver to generate a component factory from a component class name. The component factory will be used by our ViewContainerRef to generate and insert a new component into our view. After generating our component factory, we clear out the current content of our view container, and then create a new component by call createComponent, passing in our component factory.

Now we can build our library.

npm run build

 

Using Our Overlay Side Panel Component

With our library build, we can use our overlay side panel component by simply importing the OverlaySidePanelModule and place a <foo-overlay-side-panel> element on our page. We then can inject the OverlaySidePanelService into any component of that module and use that service to control the side panel.

Our ModalService public interface…

Method Return Description
open() void Opens the side panel
close() void Closes the side panel
setContent(Type<any>) void Sets the component to render
onVisibilityChange() Observable<boolean> Panel open/close event
onContentChange() Observable<Type<any>> Content changed event

NOTE: Remember, each module that imports our OverlaySidePanelModule will receives it’s own instance of our service. So each module will need it’s own <foo-overlay-side-panel> element.

 

Stackblitz Result

I’ve also embeded a Stackblitz showing our overlay side panel being used with our dashabord layout we create a few posts back. Each page has a few buttons that will toggle our side panel with different components as content.

A full screen demo can also be viewed here

 

 

The completed github repository can be found here

 


comments powered by Disqus