Custom Slide-Out Menu Component: Populated with Dynamic Component Data, and Styled with Angular Animations & SCSS
Tags: angular, typescript, webdev, javascript
Published: 2022-05-21 06:17:06

# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lk5p2h13hjeabwxk4cnp.gif)

## Part 2: [Parent-Child Component Communication with Angular and Vanilla JS](https://dev.to/riapacheco/parent-child-component-communication-with-angular-and-vanilla-js-1pae)
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 if you're n00b)](#initial-app-setup)
 - [Create New App](#create-new-app)
 - [Add Dependencies](#add-dependencies)
 - [Add Quick SCSS Package](#add-quick-scss-package)
 - [Add Material Icons the Easy Way](#add-material-icons-the-easy-way)
- [Building the Sidebar](#building-the-sidebar)
 - [Create the Component](#create-the-component)
 - [Build the Template with Placeholder Content and SCSS](#build-the-template-with-placeholder-content-and-scss)
 - [Adding and Binding Component Data](#adding-and-binding-component-data)
 - [Properties and Data Arrays](#properties-and-data-arrays)
 - [Accessing Data and Nested Data with a Namespace](#accessing-data-and-nested-data-with-a-namespace)
- [Passing Component Data through a Click Event](#passing-component-data-through-a-click-event)
 - [Add the Click Function](#add-the-click-function)
 - [Test it in your Console](#test-it-in-your-console)
- [Adding Animations](#adding-animations)
 - [Using AngularBrowserAnimations](#using-angularbrowseranimations)

---

# Initial App Setup

<small>
Read this section if you're n00b, otherwise [jump to next section](#building-the-sidebar)
</small>

## 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:
```typescript
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](https://www.npmjs.com/package/@riapacheco/yutes).
After installation, add the following imports to your `styles.scss` file
```scss
@import '~@riapacheco/yutes/yutes.scss';
@import '~@riapacheco/yutes/colors.scss';
```

OR add the following to your `angular.json` file:
```json
"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:
```html
<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:
```html
<i class="material-icons">search</i>
```
Will show up like this:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h2fvea1fgt2mwmsqi13j.png)
 
---

# 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:
```html
<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:
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`

<small>
Read the comments below to understand why I added a 'close' class to the first icon
</small>
```html
<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:
```scss
@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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wzt24iw1kjtvq453ifr8.png)

---

# 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.
```typescript
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 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
```html
<!-- 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:
```html
<!--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
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpwv5crhtqzkxlq7dfc7.png)

---

# 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.
```typescript
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:
```html
<!--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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py76cnixhvcl9b3z5exb.gif)

---

# Adding Animations
Finally, we can add animations by first adding an animation trigger (`[@triggerName]`) via  [Angular Animations](https://angular.io/guide/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`.
```html
<!--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:
```typescript
@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.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lk5p2h13hjeabwxk4cnp.gif)

Read [_Parent-Child Component Communication with Angular and Vanilla JS_](https://dev.to/riapacheco/parent-child-component-communication-with-angular-and-vanilla-js-1pae): 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

<small>
  Code for this article can be found in the `Part-1` branch of [this repo](https://github.com/riapacheco/menu-demo/tree/part-1)
</small>