Custom Slide-Out Menu Component: Populated with Dynamic Component Data, and Styled with Angular Animations & SCSS
Ria Pacheco
Posted on May 21, 2022
Objectives
Part 1 (this article)
Create a sidebar component that:
- Slides out from the left with a smooth animation
- Lists content from data found inside the component
- Emits data from that data source when an item is clicked
End Result ⤵
Part 2: Parent-Child Component Communication with Angular and Vanilla JS
- Read a parent component's data
- Emit data from the parent component's data but triggered by an event from the child sidebar
- Use that data to scroll to dynamic element IDs in the template
Skip Ahead
- Initial App Setup (read if you're n00b)
- Building the Sidebar
- Passing Component Data through a Click Event
- Adding Animations
Initial App Setup
Read this section if you're n00b, otherwise jump to next section
Create New App
Create the app by running $ ng new menu-demo --skip-tests
in your terminal. Choose y
for routing and select the SCSS
option with the styling prompt.
Add Dependencies
In the app.module.ts
file, add the BrowserAnimationsModule
and the CommonModule
so that we can manipulate directives like so:
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// Add these ⤵
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
// And ⤵
CommonModule,
BrowserAnimationsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Add Quick SCSS Package
To stay high-level, I'm using my @riapacheco/yutes
package on NPM.
After installation, add the following imports to your styles.scss
file
@import '~@riapacheco/yutes/yutes.scss';
@import '~@riapacheco/yutes/colors.scss';
OR add the following to your angular.json
file:
"projects": [
"menu-demo": [
"build": [
"options": [
"styles": {
"./node_modules/@riapacheco/yutes/yutes.scss",
"./node_modules/@riapacheco/yutes/colors.scss"
}
]
]
]
]
Add Material Icons the Easy Way
We want to use icons for the Open and Close button of the menu, so a fast way to do that is by using Google's material icon font via href. Add the following inside your <head>
within your index.html
file:
<link href="https://fonts.googleapis.com/icon family=Material+Icons"rel="stylesheet">
Now you can add icons by adding the material-icons
class to an element that names the icon type through its content like this:
<i class="material-icons">search</i>
Building the Sidebar
Create the Component
Run $ ng g c components/sidebar
in your terminal to create a new component.
Remove all the default content found in the app.component.html
file and replace it with the following selector:
<app-sidebar></app-sidebar>
Build the Template with Placeholder Content and SCSS
First, we'll add the structure of the sidebar in the template in a way that allows us (later through SCSS) to create a sidebar that doesn't move unless triggered (position: absolute
) and uses an icon toggle button that only appears when hovering over the menu. We add conditions to the toggle button in three ways:
- We use Angular's
*ngIf
directive to show the icon when we want it shown - We use SCSS to set
opacity: 0
to the icons until the the overall sidebar area has been hovered over - On the
.toggle-btn
anchor element itself, we add a(click)
directive that enables straight toggle behavior withshowsSidebar = !showsSidebar
Read the comments below to understand why I added a 'close' class to the first icon
<nav class="sidebar">
<div class="sidebar-content">
<a
(click)="showsSidebar = !showsSidebar"
class="toggle-btn">
<!--Shows Close Icon on button when sidebar is open and if hovering-->
<!-- I've added an additional class 'close' to this so that I can differentiate between icons and keep the other `menu` icon visible when the menu is closed -->
<span
*ngIf="showsSidebar"
class="material-icons close">
close
</span>
<!-- Shows Menu Icon on button when sidebar is closed-->
<span
*ngIf="!showsSidebar"
class="material-icons">
menu
</span>
</a>
<!-- This is a group of sections -->
<div class="sections">
<!-- This is a single section -->
<div class="section">
<!-- This is the title of a section -->
<a href="" class="section-title">
The Evolving State of SE
</a>
<!-- This is the list of items within a section -->
<div class="section-content">
<ul class="list-unstyled">
<li>
<a>
Definitions and Terms
</a>
</li>
<li>
<a>
Root Cause Analysis
</a>
</li>
<li>
<a>
Industry and Government
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</nav>
We'll now add the absolute
structure of the component with SCSS, as well as the opacity: 0
to the toggle button. This is reversed to opacity: 1
when the user hovers over the overall sidebar itself:
@import '~@riapacheco/yutes/colors.scss';
nav, .sidebar {
background-color: white;
border-radius: 0px 6px 6px 0px;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 330px;
box-shadow: 8px 8px 18px #00000030;
.sidebar-content {
width: 100%;
// Full width button
// Contains the icon, but uses flex-box to push to the right-side
.toggle-btn {
width: 100% !important;
height: 1.5rem;
padding-right: 0.4rem;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: flex-end;
span {
font-size: 0.99rem;
// IF the button icon has a <span> AND a `.close` class,
// THEN set its opacity to 0 [transparent]
&.close {
opacity: 0;
}
// IF the cursor hovers over the actual icon (not just the menu)
// THEN change the icon color
&:hover {
color: $secondary-color;
}
}
}
}
// IF the cursor is hovering over the sidebar menu,
&:hover {
.sidebar-content {
.toggle-btn {
// THEN change the toggle-btn's <span> element opacity: 0 to opacity: 1
span {
opacity: 1;
}
}
}
}
}
// All sections
.sections {
margin-left: 2rem;
margin-top: 2rem;
.section{
margin-bottom: 3rem;
// Section Title
.section-title {
font-size: 0.88rem;
line-height: 0.88rem;
text-transform: uppercase;
letter-spacing: 0.09rem;
font-weight: 600;
&:hover {
color: $secondary-medium-color;
}
}
// Listed items
.section-content {
ul {
margin-top: 1rem;
margin-bottom: 1rem;
margin-left: 1rem;
li {
margin-bottom: 0.5rem;
a:hover {
color: $secondary-medium-color;
}
}
}
}
}
}
Now the component looks like this:
Adding and Binding Component Data
We will need to add data to the component first so that:
- We understand the data's shape (schema / hierarchy)
- We ensure the styles applied make sense for that shape
Properties and Data Arrays
We'll add a showsSidebar
property to drive the initial behavior of the sidebar and set it to true
. Then we'll add a data array of objects called sections
that we'll populate right inside the component.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss']
})
export class SidebarComponent implements OnInit {
// Toggles the sidebar from view
showsSidebar = true;
// Populated data so that we understand the shape / schema
sections = [
{
sectionHeading: 'The Evolving State of SE',
sectionTarget: 'theEvolvingStateOfSe',
sectionContents: [
{
title: 'Definitions and Terms',
target: 'definitionsAndTerms',
},
{
title: 'Root Cause Analysis',
target: 'rootCauseAnalysis',
},
{
title: 'Industry and Government',
target: 'industryAndGovernment',
},
{
title: 'Engineering Education',
target: 'engineeringEducation',
},
{
title: 'Chapter Exercises',
target: 'chapterExercises'
}
]
},
{
sectionHeading: 'Attributes and Properties',
sectionTarget: 'attributesAndProperties',
sectionContents: [
{
title: 'Definitions and Terms',
target: 'defintionAndTerms',
},
{
title: 'User Roles and Missions',
target: 'userRolesAndMissions',
},
{
title: 'Defining User Missions',
target: 'definingUserMissions',
},
{
title: 'Problem, Opportunity, Solution',
target: 'problemOpportunitySolution',
},
{
title: 'Spaces',
target: 'spaces'
},
]
}
];
constructor() { }
ngOnInit(): void {
}
}
It's important to remember that the first layer includes a
sectionTarget
to the type ofstring
. And similarly the second nested layer (list of items undersectionContents
) includes atarget
string. These were added as a reference string to be passed through later.
Accessing Data and Nested Data with a Namespace
Here's where we'll use Angular's *ngFor
directive to bind the first array layer of data to a repeating element within the template
<!-- Earlier Code -->
<div class="sections">
<!-- ------------------- This is where we bind the data -------------------- -->
<div
*ngFor="let section of sections;"
class="section">
<!--Section Title-->
<a href="" class="section-title">
{{ section.sectionHeading }}
</a>
<!-- more code -->
Now that we've established a namespace (e.g. section.sectionHeading
) for accessing data. We can use that namespace a second time to access within a nested array:
<!--Earlier Code-->
<div class="sections">
<!-- ------------------- This is where we bind the data -------------------- -->
<div
*ngFor="let section of sections;"
class="section">
<!--Section Title-->
<a href="" class="section-title">
{{ section.sectionHeading }}
</a>
<!-- ------------- This is where we access the nested content -------------- -->
<div
*ngFor="let sectionContent of section.sectionContents"
class="section-content">
<ul class="list-unstyled">
<li>
<a>
{{ sectionContent.title }}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</nav>
Now the data from the Component is reflected in the view
Passing Component Data through a Click Event
Relevant component data, passed through a click event, is important since it enables context. Essentially, we get to play with the data that's directly related to the element we clicked (regardless of the template being dynamically populated).
It's handy for parent component communication (part 2 to this article) and for use-cases where you want to line up the data that's passed through so that a follow-up function (e.g. scrollTo()
) might catch the string and use it to scroll to the right element in a template, identified by an elementRef
directive.
Add the Click Function
In the template, add a function called onTargetContentClick()
, that accepts a string and an event, so that we can show its result in the console.
export class SidebarComponent implements OnInit {
// More code
onTargetContentClick(targetString: string, event: Event) {
console.log(targetString);
}
}
Remember that every "section" has a "sectionTarget" string and every sectionContent has a "target" string. Add the function to the template like this:
<!--Earlier Code-->
<div class="sections">
<div
*ngFor="let section of sections;"
class="section">
<!-- Add the function to the Section title -->
<a
(click)="onTargetContentClick(section.sectionTarget, $event)"
class="section-title">
{{ section.sectionHeading }}
</a>
<div
*ngFor="let sectionContent of section.sectionContents"
class="section-content">
<ul class="list-unstyled">
<li>
<!--Add the function to every listed item's anchor element-->
<a (click)="onTargetContentClick(sectionContent.target, $event)">
{{ sectionContent.title }}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</nav>
Test it in your Console
Now you can open your Chrome devTools console (either by clicking F12
on your keyboard or command+alt+i
, followed by selecting the console
tab) and click each item to see what appears:
Adding Animations
Finally, we can add animations by first adding an animation trigger ([@triggerName]
) via Angular Animations
By adding the trigger followed by a ternary operator we're essentially saying: if the isOpen
property is true, the [@sidebarTrigger]
will use the open
state defined in the component file, else use close
.
<!--Add an animation trigger followed by a ternary operator-->
<nav
[@sidebarTrigger]="isOpen ? 'open' : 'close'"
class="sidebar">
<div class="sidebar-content">
<a
(click)="showsSidebar = !showsSidebar"
class="toggle-btn">
<span
*ngIf="showsSidebar"
class="material-icons close">
close
</span>
<span
*ngIf="!showsSidebar"
class="material-icons"
style="opacity: 1 !important;">
menu
</span>
</a>
Using AngularBrowserAnimations
Now we add the animations by importing the decorators from the package, and adding the array to the @Component
decorator like so:
@Component({
selector: 'app-sidebar',
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss'],
animations: [
trigger('sidebarTrigger', [
// To add a cool "enter" animation for the sidebar
transition(':enter', [
style({ transform: 'translateX(-100%)' }),
animate('300ms ease-in', style({ transform: 'translateY(0%)' }))
]),
// To define animations based on trigger actions
state('open', style({ transform: 'translateX(0%)' })),
state('close', style({ transform: 'translateX(-94%)' })),
transition('open => close', [
animate('300ms ease-in')
]),
transition('close => open', [
animate('300ms ease-out')
])
])
]
})
Completed Standalone Sidebar
We now have a standalone sidebar that reads data from its component and can pass that data through click events.
Read Parent-Child Component Communication with Angular and Vanilla JS: we'll make the sidebar reusable using Angular's @Input()
and @Output()
decorators, use EventEmitter
to pass the parent's data through the child's (click)
event, and use that same data to scroll to dynamic IDs in the template!
Stay tuned.
Ri
Code for this article can be found in the Part-1
branch of this repo
Posted on May 21, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.