Custom/Reusable Toggle Component (with Angular Animations)

Ria Pacheco - Apr 30 '22 - - Dev Community

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


Create a Starting Toggle

First, to drive the behaviour of the toggle, add a state property to the actual component:

// toggle.component.ts
export class ToggleComponent implements OnInit {  
toggleOn = false;  
...
}
Enter fullscreen mode Exit fullscreen mode

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:

<!--toggle.component.html-->
<a class="toggle-container">
  <div class="toggle-bg">
    <div class="toggle"></div>  
  </div>
</a>
Enter fullscreen mode Exit fullscreen mode

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:

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

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

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({  
  declarations: [...],  
  imports: [
    ...
    BrowserAnimationsModule
  ],  
  providers: [...],  
  bootstrap: [AppComponent]
})
Enter fullscreen mode Exit fullscreen mode

Add an Animation Trigger to Template File

In your toggle.component.html file, add a trigger by adding [@] 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:

<!--toggle.component.html-->
<a class="toggle-container">
  <div class="toggle-bg">    
    <div 
      [@toggleTrigger]="toggleOn ? 'on' : 'off'" 
      class="toggle">
    </div>
  </div>
</a>
Enter fullscreen mode Exit fullscreen mode

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

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

// toggle.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
Enter fullscreen mode Exit fullscreen mode

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:

export class ToggleComponent implements OnInit {  
@Input() toggleOn = false;  
@Output() toggledTo = new EventEmitter();
...
}
Enter fullscreen mode Exit fullscreen mode

Then we add a conditional function which will trigger the toggling of on and off, and send its states to the parent component:

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

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).

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

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:

<!--parent.component.html-->
<app-toggle (toggleTo)="onToggleClick($event)"></app-toggle>
Enter fullscreen mode Exit fullscreen mode
// parent.component.ts
export class ParentComponent implements OnInit {
...  

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

Full Code

Toggle SCSS File

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

Toggle Template File

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

Toggle Component File

toggle.component.ts

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

Parent Template File

parent.component.html

<app-toggle (toggledTo)="onEditorToggle($event)"></app-toggle>

Enter fullscreen mode Exit fullscreen mode

Parent Component File

parent.component.ts

export class ParentComponent implements OnInit {  

  constructor() { }  

  ngOnInit(): void {  }

  onEditorToggle(value): void {
    console.log(value);  
  }
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . .