Custom Slide-Out Menu Component: Populated with Dynamic Component Data, and Styled with Angular Animations & SCSS

riapacheco

Ria Pacheco

Posted on May 21, 2022

Custom Slide-Out Menu Component: Populated with Dynamic Component Data, and Styled with Angular Animations & SCSS

Objectives

Part 1 (this article)

Create a sidebar component that:

  1. Slides out from the left with a smooth animation
  2. Lists content from data found inside the component
  3. Emits data from that data source when an item is clicked

End Result ⤵

Image description

Part 2: Parent-Child Component Communication with Angular and Vanilla JS

  1. Read a parent component's data
  2. Emit data from the parent component's data but triggered by an event from the child sidebar
  3. Use that data to scroll to dynamic element IDs in the template

Skip Ahead


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 { }
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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"
        }
      ]
    ]
  ]
]
Enter fullscreen mode Exit fullscreen mode

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Will show up like this:
Image description


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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. We use Angular's *ngIf directive to show the icon when we want it shown
  2. We use SCSS to set opacity: 0 to the icons until the the overall sidebar area has been hovered over
  3. On the .toggle-btn anchor element itself, we add a (click) directive that enables straight toggle behavior with showsSidebar = !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>
Enter fullscreen mode Exit fullscreen mode

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;
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now the component looks like this:
Image description


Adding and Binding Component Data

We will need to add data to the component first so that:

  1. We understand the data's shape (schema / hierarchy)
  2. 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 {
  }

}
Enter fullscreen mode Exit fullscreen mode

It's important to remember that the first layer includes a sectionTarget to the type of string. And similarly the second nested layer (list of items under sectionContents) includes a target 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 -->
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Now the data from the Component is reflected in the view
Image description


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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

Image description


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>
Enter fullscreen mode Exit fullscreen mode

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')
      ])
    ])
  ]
})
Enter fullscreen mode Exit fullscreen mode

Completed Standalone Sidebar

We now have a standalone sidebar that reads data from its component and can pass that data through click events.
Image description

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

💖 💪 🙅 🚩
riapacheco
Ria Pacheco

Posted on May 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related