Creating an Angular Component Library - Overlay Side Panel Component
Published on
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
- Install Node
- Install Angular CLI
- Creating an Angular Component Library - Workspace Setup
- OR Clone this github repository
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()">×</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.
The completed github repository can be found here
More Posts
- Creating an Angular Component Library - Workspace Setup
- Creating an Angular Component Library - Alert Component
- Creating an Angular Component Library - Progress Bar Component
- Creating an Angular Component Library - Toaster Component
- Creating an Angular Component Library - Card Component
- Creating an Angular Component Library - Flip Card Component
- Creating an Angular Component Library - Overlay Loader Component
- Creating an Angular Component Library - Overlay Side Panel Component
- Creating an Angular Component Library - Button Component
- Creating an Angular Component Library - Toggle Switch Component
- Creating an Angular Component Library - Checkbox Component