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
- Create a starting toggle with two divs that layer on themselves using SCSS/CSS
- Add animation to the toggle using Angular Animations package
- Change the toggle background color, based on current toggle state
- 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;
...
}
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>
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;
}
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]
})
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>
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')
])
])
]})
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:
- Based on the current state of the toggle, be able to switch it from on to off and vice versa
- 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';
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();
...
}
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');
}
}
}
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>
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>
// 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
@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
<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
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
<app-toggle (toggledTo)="onEditorToggle($event)"></app-toggle>
Parent Component File
parent.component.ts
export class ParentComponent implements OnInit {
constructor() { }
ngOnInit(): void { }
onEditorToggle(value): void {
console.log(value);
}
}