Advertisement

#9 Animating the Sub-Actions

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.

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

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
(a) We need to modify our template to bind a click handler to our forgot links and the reset button.

authenticator.component.scss

.authenticator {
    background-color: #1b1b1b;
    
    a {
        cursor: pointer;
    }

    .inputs-container {
        background-color: white;
    }
}
(b) Small addition to our styles.

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");
    }
    ...
}
(c) We have bound event handlers in the template so now we need to create them in our typescript class.

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

Console output that shows that the three new methods that we have created
            are being called correctly when the corresponding anchor is clicked.
(d) Console output that shows that the three new methods that we have created are being called correctly when the corresponding anchor is clicked.

We need to read the DOM

  • WebUi
    • Source
      • services
        • dom-reader.service.ts

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;
    }
}
(e) Our small service, that we will be adding to later, that we can use to find DOM elements.
  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts

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);
        };
    }
}
(f) Additional code need to animate our inputs container.
Exciton Interactive LLC
Advertisement