Custom/Reusable Toggle Component (with Angular Animations)
Tags: angular, webdev, javascript, typescript
Published: 2022-04-30 04:18:44

Had to create a toggle recently from scratch and, given the features that come with Angular, it was super easy and fast to do. Thought I'd share.

### Workflow
1. Create a starting toggle with two divs that layer on themselves using SCSS/CSS
2. Add animation to the toggle using Angular Animations package
3. Change the toggle background color, based on current toggle state
4. Emit state to parent component

### Jump ahead
- [Creating a Starting Toggle](#creating-a-starting-toggle)
- [Using Angular Animations Instead of CSS](#using-angular-animations-instead-of-css)
 - [Add BrowserAnimationsModule](#add-browseranimationsmodule)
 - [Add an Animation Trigger to Template File](#add-an-animation-trigger-to-template-file)
 - [Animation State Behavior](#animation-state-behavior)
- [Add Click Event](#add-click-event)
- [Full Code](#full-code)
 * [Toggle SCSS File](#toggle-scss-file)
 * [Toggle Template File](#toggle-template-file)
 * [Toggle Component File](#toggle-component-file)
 * [Parent Template File](#parent-template-file)
 * [Parent Component File](#parent-component-file)



---

# Create a Starting Toggle
First, to drive the behaviour of the toggle, add a state property to the actual component:
```typescript
// toggle.component.ts
export class ToggleComponent implements OnInit {  
toggleOn = false;  
...
}
```

In the template, add a container div, an inner div (to act as the background color), and an inner-inner div to act as the actual toggle square:
```html
<!--toggle.component.html-->
<a class="toggle-container">
  ﻿<div class="toggle-bg">
    <div class="toggle"></div>  ﻿
  </div>
</a>
```

To get a div to appear over another div (and stay within that behind div's boundaries), you'll want to make the give the background div's position: relative and the foreground div's position: absolute. Remember that absolute only works when you've added a x and y axis keys like this:

```scss
@import '~./src/app/scss/colors.scss';

.toggle-bg {  
  display: inline-block;  
  height: 1rem;  
  width: 2rem;  
  background-color: $accent-color;  
  border-radius: 3px;  
  position: relative;

  .toggle {    
    width: 1rem;    
    display: inline-block;    
    background-color: white;    
    position: absolute;    
    left: 0.01rem;    
    top: 0;    
    bottom: 0;    
    margin: 0.1rem;    
    border-radius: 3px;    
    box-shadow: 2px 2px 12px #00000050;  
  }
}

.toggle-on {
  background-color: $primary-color;
}
```

Notice that I only referred to a state change of color (no animations). We'll add this later.

---

# Using Angular Animations Instead of CSS
I like using Angular Animations since (like most of their features) they're state-driven instead of just being event driven. If we just track a click event, there might be a case where the click order gets out of sync and 'on' might not mean 'on' anymore.

### Add BrowserAnimationsModule
In you `app.module.ts` file add
```typescript
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';﻿

@NgModule({  
  declarations: [...],  
  imports: [
    ...
    BrowserAnimationsModule
  ],  
  providers: [...],  
  bootstrap: [AppComponent]
})
```

### Add an Animation Trigger to Template File
In your toggle.component.html file, add a trigger by adding [@<trigger-name>] to the element you want to animate (in this case, the toggle that moves). This is followed by a ternary operator that takes the state property we created earlier (toggleOn = false;) and provides an 'if/else' outcome based on that property's condition:
```html
<!--toggle.component.html-->
﻿<a class="toggle-container">
  <div class="toggle-bg">    
    <div 
      [@toggleTrigger]="toggleOn ? 'on' : 'off'" 
      class="toggle">
    </div>
  ﻿</div>
</a>
```
This means: if toggleOn is true, then the state of this animation is on, else the state of this animation is off.

### Animation State Behavior
Now we apply the behaviours that happen when the toggle is 'on' or 'off' in the actual component.
* First we import the animation functions from the @angular/animations package (kind of annoying but whatever)
* Then we add the animation trigger, states, and behaviour (with styles) to the actual component metadata
```typescript
import { Component, OnInit } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({  
  selector: 'app-toggle',  
  templateUrl: './toggle.component.html',  
  styleUrls: ['./toggle.component.scss'],  
  animations: [
    // First we add the trigger, which we added to the element in square brackets in the template    
    trigger('toggleTrigger', [
    // We define the 'off' state with a style -- translateX(0%), which does nothing﻿      
    state('off', style({ transform: 'translateX(0%)' })),
    // We define the 'on' state with a style -- move right (on x-axis) by 70%      
    state('on', style({ transform: 'translateX(70%)' })),
    // We define a transition of on to off (and vice versa) using `<=>`      
    transition('on <=> off', [
    // We add the time (in milliseconds) and style of movement with `animate()`        
    animate('120ms ease-in-out')      
    ])    
  ])  
]})
```

---

# Add Click Event

Because we want to be able to track if the state is either 'on' or 'off' in the future, we won't want to use a simple (click)="toggleOn = !toggleOn". Instead, we'll create a new function called toggleClick() which will do two things:
1. Based on the current state of the toggle, be able to switch it from on to off and vice versa
2. Emit the actual state with a string to any parent component using it

To do this, we want to import the @Output() property and the EventEmitter method to the component from @angular/core
```typescript
// toggle.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
```

We add this to the component class, and name the EventEmitter so that we have something to emit values in the first place. We also want to add @Input() property to the toggleOn property so that a parent can access it:
```typescript
export class ToggleComponent implements OnInit {  
@Input() toggleOn = false;  
@Output() toggledTo = new EventEmitter();
...
}
```

Then we add a conditional function which will trigger the _toggling_ of on and off, and send its states to the parent component:
```typescript
export class ToggleComponent implements OnInit {  
@Input() toggleOn = false;  
@Output() toggledTo = new EventEmitter();  

constructor() { }  

ngOnInit(): void {  }

// We will have the `toggleTo` EventEmitter emit a string  toggleClick(): any {    
  if (this.toggleOn) {      
    this.toggleOn = false;      
    this.toggledTo.emit('off');    
  } else {      
      this.toggleOn = true;      
      this.toggledTo.emit('on');    
    }  
  }
}
```

Also, since the state is now rigidly defined, we can add a conditional [ngClass] to the toggle-bg div, so that it knows to add the class 'toggle-on' when the state is toggleOn and consequently change the background color (as in the SCSS file).
```html
<a 
  class="toggle-container" 
  (click)="toggleClick()">  

  <div
    [ngClass]="toggleOn ? 'toggle-bg toggle-on' : 'toggle-bg'">    
    ﻿<div [@toggleTrigger]="toggleOn ? 'on' : 'off' " class="toggle">
    </div>  
  <div>
</a>
```
Again, the boys at Angular love ternary operators -- which is great. So with [ngClass], we're saying that IF the state is toggleOn (true), then use the classes toggle-bg toggle-on ELSE just use toggle-bg.

Now you can add it to a parent component and create a function within that parent which will catch the emitted value:
```html
<!--parent.component.html-->
<app-toggle (toggleTo)="onToggleClick($event)"></app-toggle>
```
```typescript
// parent.component.ts
export class ParentComponent implements OnInit {
...  

onToggleClick(value): void {    
  console.log(value);
  // will print 'on' or 'off' depending on state  
  }
```

---

# Full Code

#### Toggle SCSS File
`toggle.component.scss`
```scss
@import '~./src/app/scss/colors.scss';

.toggle-bg {  
  display: inline-block;  
  height: 1rem;  
  width: 2rem;  
  background-color: $accent-color;  
  border-radius: 3px;  
  position: relative;

  .toggle {    
    width: 1rem;    
    display: inline-block;    
    background-color: white;    
    position: absolute;    
    left: 0.01rem;    
    top: 0;    
    bottom: 0;    
    margin: 0.1rem;    
    border-radius: 3px;    
    box-shadow: 2px 2px 12px #00000050;  
  }
}

.toggle-on {
  background-color: $primary-color;
}
```

#### Toggle Template File
`toggle.component.html`
```html
<a 
  class="toggle-container" 
  (click)="toggleClick()">  
  <div
    [ngClass]="toggleOn ? 'toggle-bg toggle-on' : 'toggle-bg'">    
  ﻿  <div [@toggleTrigger]="toggleOn ? 'on' : 'off' " class="toggle"></div>  
  ﻿</div>
</a>
```

#### Toggle Component File
`toggle.component.ts`
```typescript
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({  
  selector: 'app-toggle',  
  templateUrl: './toggle.component.html',  
  styleUrls: ['./toggle.component.scss'],  
  animations: [    
    trigger('toggleTrigger', [      
      state('off', style({ transform: 'translateX(0%)' })),      
      state('on', style({ transform: 'translateX(70%)' })),      
      transition('on <=> off', [        
        animate('120ms ease-in-out')      
      ])    
    ])  
]})

export class ToggleComponent implements OnInit {  
@Input() toggleOn = false;  
@Output() toggledTo = new EventEmitter();  

constructor() { }  

ngOnInit(): void {  }  

toggleClick(): any {    
  if (this.toggleOn) {      
    this.toggleOn = false;      
    this.toggledTo.emit('off');    
  } else {      
    this.toggleOn = true;      
    this.toggledTo.emit('on');    
  }  
}}
```

#### Parent Template File
`parent.component.html`
```html
<app-toggle (toggledTo)="onEditorToggle($event)">﻿</app-toggle>

```

#### Parent Component File
`parent.component.ts`
```typescript
export class ParentComponent implements OnInit {  

  constructor() { }  

  ngOnInit(): void {  }

  onEditorToggle(value): void {
    console.log(value);  
  }
}﻿
```