#9 Animating the Sub-Actions
Friday, December 1, 2017
In this article we will create the animation that will cause our inputs container to move out of view when a user clicks the forgot username or forgot password links and then it will animate back into view. In future articles we will hide/show the appropriate inputs in our interface while the container is hidden off screen before it animates back into view.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
You can view a completed example of the authenticator component here.
Our first event handler(s)
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
We start by modifying our template file (a) so that we can react to the user clicking the forgot username link, the forgot password link and the
reset button. To do this we simply need to add code to bind an event handler to the click event by adding code of the form (click)="clickHandler()"
to each of the links and button mention previously. Next we will add just a small amount of styling to the component's scss file (b).
Lastly, now that we have added the event handlers to the template we need to add methods with the corresponding names to our typescript file
(c). In each of the methods we are using our old friend the console log to make sure that we have wired everything up
correctly.
authenticator.component.pug
div.authenticator
div.inputs-container#inputs-container
h3 Login
form(novalidate, [formGroup]=formName)
...
div.forgot
a((click)="onClickForgotUsername()") Forgot your username?
...
div.forgot
a((click)="onClickForgotPassword()") Forgot your password?
...
div.input-buttons
button((click)="onClickReset()") Reset
authenticator.component.scss
.authenticator {
background-color: #1b1b1b;
a {
cursor: pointer;
}
.inputs-container {
background-color: white;
}
}
authenticator.component.ts (1)
export class AuthenticatorComponent implements OnInit {
...
public onClickForgotPassword = () => {
console.log("forgot password");
}
public onClickForgotUsername = () => {
console.log("forgot username");
}
public onClickReset = () => {
console.log("reset");
}
...
}
Once we save our files and return to the browser and refresh we will see, once we click on the anchors or reset button, we should see output of the type shown in (d).
We need to read the DOM
-
WebUi
-
Source
-
services
- dom-reader.service.ts
-
services
-
Source
In order to perform the animations that we want we need to have a reference to the DOM element that we are trying to animate. We have of course already talked about trying to limit the size of our bundles as much as possible so we don't want to include an addition library like jQuery just to find DOM elements. What we will do is create another small service that we will use to find the DOM elements that we are looking for. So with that said we will create a new file within our services folder called 'dom-reader.service.ts'. For now we will just create a couple of public methods on our service which are designed to find a child of a provided parent element either by a general query selector or specifically by an id (e).
dom-reader.service.ts
export class DOMReaderService {
public findChildById<T extends HTMLElement>(parent: HTMLElement, id: string, allowNil: boolean = false) {
id = this.parseId(id);
return this.findChild(parent, `[id="${id}"]`, allowNil) as T;
}
public findChild<T extends HTMLElement>(parent: HTMLElement, querySelector: string, allowNil: boolean = false) {
const child = parent.querySelector(querySelector);
if (typeof child === "undefined" || child === null) {
if (allowNil) {
return null;
}
console.error(parent);
throw new Error(`Parent does not contain an element with query selector: ${querySelector}.`);
}
return child as T;
}
private parseId = (id: string) => {
if (typeof id === "undefined" || id === null || id === "") {
throw new Error("Cannot find an element using a nil or empty id.");
}
if (id[0] === "#") {
id = id.substring(1);
}
return id;
}
}
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
With our DOM reader service we now have everything in place in order to animate our inputs container. We will start with importing the
ElementRef
which will give us access to the DOM of our component and of course the
DOMReaderService
. From there we need to remember to include the service in our providers array
so that angular will inject an instance into our constructor. We also add a few private fields and properties as well as a couple of public
getters. The the magic is shown in the subActionAnimation
method where we are using
the animation api, either through native support of the browser or from the animation polyfill that we imported at the start of this
project, to animate the opacity and transform of the inputs container (f).
authenticator.component.ts (2)
import { Component, ElementRef, OnInit } from "@angular/core";
...
import { DOMReaderService } from "../../services/dom-reader.service";
...
@Component({
...
providers: [DOMReaderService, FormBuilder, UriParserService]
})
export class AuthenticatorComponent implements OnInit {
...
private _domInputsContainer: HTMLDivElement = null;
private _isSubActionForgotPassword: boolean = false;
private _isSubActionForgotUsername: boolean = false;
...
private get domInputsContainer() {
if (this._domInputsContainer != null) {
return this._domInputsContainer;
}
this._domInputsContainer = this._domReader.findChildById<HTMLDivElement>(this._elementRef.nativeElement, "inputs-container");
return this._domInputsContainer;
}
...
public get isSubActionForgotPassword() { return this._isSubActionForgotPassword; }
public get isSubActionForgotUsername() { return this._isSubActionForgotUsername; }
...
constructor(
private readonly _domReader: DOMReaderService, private readonly _elementRef: ElementRef,
private readonly _uriParser: UriParserService, fb: FormBuilder
) {
...
}
...
public onClickForgotPassword = () => {
this._isSubActionForgotPassword = true;
this.subActionAnimate(() => {
});
}
public onClickForgotUsername = () => {
this._isSubActionForgotUsername = true;
this.subActionAnimate(() => {
});
}
public onClickReset = () => {
if (this.isSubActionForgotPassword) {
this._isSubActionForgotPassword = false;
this.subActionAnimate(() => {
});
return;
}
if (this.isSubActionForgotUsername) {
this._isSubActionForgotUsername = false;
this.subActionAnimate(() => {
});
return;
}
}
...
private subActionAnimate = (action: () => void) => {
const width = this.domInputsContainer.clientWidth;
const timing: AnimationEffectTiming = {
duration: 500,
easing: "ease-in-out",
fill: "forwards"
};
const outAnimation = this.domInputsContainer.animate(
{
opacity: [1, 0],
transform: ["translate(0)", `translate(-${width}px)`]
}, timing);
outAnimation.onfinish = () => {
action();
setTimeout(() => {
this.domInputsContainer.animate(
{
opacity: [0, 1],
transform: [`translate(-${width}px)`, "translate(0)"]
},
timing);
}, 500);
};
}
}