Advertisement

#17 Start Animating the Info Pages

In this article we will begin the process of animating the info pages of our component's user interface. An info page is simple the information that is shown when a user focuses a particular input. The information displayed is mainly the validators for that particular input but in the future it will also contain the errors, if any, related to that input that have been returned by the server. By the time we are done with this article we will be able to play a show animation for the corresponding info page when an input is focused and play a hide animation when the focus is removed.

You can view a completed example of the authenticator component here.

Can never have too many ids

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.pug
          • authenticator.component.ts

We begin as we have previously by applying a unique id to all of our info pages. The first step is to modify our info mixin in the template file so that we can bind to the id attribute the value of a specified property of our component's typescript class (a). With the template modified we need to now create the properties that will return to us the ids that we should be binding (b).

authenticator.component.pug (1)

...
mixin info(heading, key)
    div.info([attr.id]=`${key}InfoId`)
        ...
div.authenticator
    ...
    div.info-container
        div.info-pages
            +info("Welcome!", "default")
                ...
            +info("Username", controlUsernameKey)
                ...
            +info("Password", controlPasswordKey)
                ...
            +info("Confirm Password", controlPasswordConfirmKey)
                ...
            +info("Email Address", controlEmailKey)
                ...
            +info("Confirm Email Address", controlEmailConfirmKey)
                ...
(a) Modifying our template in order to bind a property on our typescript class to the id attribute of each info page.

authenticator.component.ts (1)

...
export class AuthenticatorComponent implements ... {
    ...
    public get controlEmailInfoId() { return "info-email"; }
    public get controlEmailConfirmInfoId() { return "info-email-confirm"; }
    public get controlPasswordInfoId() { return "info-password"; }
    public get controlPasswordConfirmInfoId() { return "info-password-confirm"; }
    public get controlRememberMeInfoId() { return "info-remember-me"; }
    public get controlUsernameInfoId() { return "info-username"; }
    ...
    public get defaultInfoId() { return "info-default"; }
    ...
}
(b) Properties on our typescript class that will return to us the ids that we should be binding to our info pages.

Once we have modified and saved both files if we look at the elements of our DOM in the developer tools of our browser we should see the ids being applied as shown in (c).

Image of the elements within the developer console of our browser showing that the 
            ids are in fact being applied to our info page elements.
(c) Image of the elements within the developer console of our browser showing that the ids are in fact being applied to our info page elements.

We need references to the dom elements

  • WebUi
    • Source
      • components
        • authenticator.component.ts
        • control-group.ts
        • control-pair-collection.ts

By now we are also familiar with the process of gaining access to the DOM elements within our typescript classes which we are going to use again for our info pages. We start by creating the usual lazy getter within our control group class which will use the info id that we will pass in to the constructor (d). We are going to make it optional since we do not have an info page, at least for now, for the remember me control group. Next since we are creating our control groups through a method on the control pair collection we need to modify both the create pair config interface as well as the objects that we are passing into the constructors (e). With these changes made we can now make changes to our component's typescript file in order to pass in the id to the info page when creating our groups (f).

control-group.ts (1)

...
export interface IControlGroupConfig {
    ...
    infoId?: string;
}

export class ControlGroup {
    ...
    private readonly _infoId: string = null;
    ...
    private _infoDOM: HTMLElement = null;
    ...
    private get infoDOM() {
        if (typeof this._infoId === "undefined") {
            throw new Error(`Trying to access the info DOM for ${this._id} which is undefined.`);
        }
        if (this._infoDOM !== null) {
            return this._infoDOM;
        }
        this._infoDOM = this._domReader.findChildById(this._elementRef.nativeElement, this._infoId);
        return this._infoDOM;
    }
    ...
    constructor(config: IControlGroupConfig) {
        ...
        this._infoId = config.infoId;

        if (this._infoId) { console.log(this.infoDOM); }
    }
}
(d) We need to include the id for the info page, if any, when creating a control group.

control-pair-collection.ts (1)

...
export interface ICreatePairConfig {
    ...
    infoId?: string;
}

export class ControlPairCollection {
    public create = (...) => {
        const leftConfig = {
            ...
            infoId: left.infoId
        } as IControlGroupConfig;
        ...
        const pair = new ControlPair({
            ...
            right: {
                ...
                infoId: right.infoId
            }
        });
        ...
    }
}
(e) We need to include the id for the info page, if any, when creating a control pair.

authenticator.component.ts (2)

...
export class AuthenticatorComponent implements ... {
    ...
    public ngAfterViewInit(): void {
        this._inputPairCollection.create(
            {
                ...
                infoId: this.controlEmailInfoId
            },
            {
                ...
                infoId: this.controlEmailConfirmInfoId
            });
        this._inputPairCollection.create(
            {
                ...
                infoId: this.controlPasswordInfoId
            },
            {
                ...
                infoId: this.controlPasswordConfirmInfoId
            });
        ...
        this._inputPairCollection.create({
            ...
            infoId: this.controlUsernameInfoId
        });
    }
    ...
}
(f) Now we need to pass in the info id where appropriate.

Again once we have modified our files and saved them we can check the browser and we should see that the html elements that make up our info pages are displayed in the console (g).

Console output showing the html elements of the info pages.
(g) Console output showing the html elements of the info pages.

Once we are sure that all of the info pages are being displayed in the console we can remove the if (this._infoId) { console.log(this.infoDOM); } statement from our control group's constructor.

Advertisement

Show and hide the correct info page

  • WebUi
    • Source
      • components
        • control-group.ts
        • control-pair.ts
        • control-pair-collection.ts

The way that we are going to know whether or not to show a specific info page is by responding to the user focusing a control group's focus element. We an element is focused we will need a method on our control group to perform the animation along with a method to perform the hide animation (h). The way that we will identify which control group that the info that we want to display or hide belongs to is by passing in the group id to either the showInfo or hideInfo methods on our control pair class (i). While we are at it finding a particular control group within a control pair is a recurring need so we will create a helper method that we can call in order to do it. Our next step is of course to modify the control pair collection to add the showInfo and hideInfo methods (j). For testing purposes we will simply adjust the color of an info page instead of showing or hiding them.

control-group.ts (2)

...
export class ControlGroup {
    ...
    public hideInfo = () => {
        return this.infoDOM.animate(
            {
                color: ["yellow", "#dedede"]
            },
            {
                duration: 350,
                easing: "ease-in-out",
                fill: "forwards"
            });
    }

    public showInfo = () => {
        return this.infoDOM.animate(
            {
                color: ["#dedede", "yellow"]
            },
            {
                duration: 350,
                easing: "ease-in-out",
                fill: "forwards"
            });
    }
}
(h) Modification of our control group class so that we can easily perform the hide and show info animations easily.

control-pair.ts (1)

...
export class ControlPair {
    ...
    public focus = (...) => {
        this.find(id).focus();
    }

    public hideInfo = (id: string) => {
        return this.find(id).hideInfo();
    }

    public showInfo = (id: string) => {
        return this.find(id).showInfo();
    }

    private find = (id: string) => {
        switch (id) {
            case this._left.id:
                return this._left;
            case this._right.id:
                return this._right;
            default:
                throw new Error(`The provided id: ${id} does not match either left: ${this._left.id} or right: ${this._right} input group's id.`);
        }
    }
}
(i) In order to interact with a control group we need to create corresponding methods on our control pair.

control-pair-collection.ts (2)

...
export class ControlPairCollection {
    ...
    public hideInfo = (id: string) => {
        return this._pairs[id].hideInfo(id);
    }

    public showInfo = (id: string) => {
        return this._pairs[id].showInfo(id);
    }
}
(j) In order to interact with a control pair we need to create corresponding methods on our control pair collection.

Did someone mention focus

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.pug
          • authenticator.component.ts

As we have just mentioned previously in order to determine which info page we should be displaying we will respond to the focus event within our control groups. The first step in this process is to modify our template, specifically the form control group mixin, so that if the user focuses and input the onFocusInput method is called and the key as a string value is passed in to it (k). With the event binding completed in the template we need to create the corresponding event handler within our typescript class (l). We are also going to make it easier for us to convert a control key to a group id by using a map which we will just configure within the constructor of our authenticator component. Our first crack at showing the correct info page will be to just call the show info method of the control pair collection class with the appropriate group id when and input is focused and the hide info method we the input is blurred.

authenticator.component.pug (2)

...
mixin formControlGroup(label, inputType, key)
    div.form-control-group(...)
        ...
        div.input-group(...)
            input(..., (focus)=`onFocusInput('${key}')`)
(k) Binding an event handler to the focus event of a form control groups input.

authenticator.component.ts (3)

...
export class AuthenticatorComponent implements ... {
    ...
    private readonly _keyToGroupIdMap = new Map<string, string>();
    ...
    constructor(...) {
        ...
        this._keyToGroupIdMap[this.controlEmailKey]           = this.controlEmailGroupId;
        this._keyToGroupIdMap[this.controlEmailConfirmKey]    = this.controlEmailConfirmGroupId;
        this._keyToGroupIdMap[this.controlPasswordKey]        = this.controlPasswordGroupId;
        this._keyToGroupIdMap[this.controlPasswordConfirmKey] = this.controlPasswordConfirmGroupId;
        this._keyToGroupIdMap[this.controlRememberMeKey]      = this.controlRememberMeGroupId;
        this._keyToGroupIdMap[this.controlUsernameKey]        = this.controlUsernameGroupId;
    }
    ...
    public onBlurInput = (...) => {
        ...
        this._controlPairCollection.hideInfo(this._keyToGroupIdMap[key]);
    }

    public onFocusInput = (key: string) => {
        this._controlPairCollection.showInfo(this._keyToGroupIdMap[key]);
    }
}
(l) Creating a mapping between a control groups key and its info group id and implementing basic show and hide functionality in our focus and blur input event handlers.

With those changes made if we focus and element we see that the corresponding info page changes it's color to yellow and when the focus is removed it goes back to its default gray color (m).

When we focus an input the color of the corresponding info page is set to yellow
            and when the input is blurred the color returns to gray.
(m) When we focus an input the color of the corresponding info page is set to yellow and when the input is blurred the color returns to gray.

We also need to animate the default info page

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • control-group.ts
          • control-pair-collection.ts

In addition to the info pages that are attached to a control group we are also going to need to animate the default page as well. We can of course approach this a couple of different ways. Way one would be to abstract our control group further so that we can create a 'control group' for the default info page. Way two, which is the path we are going to take, is to pass in all of the animation settings to our control groups so that they will all be the same and we can use them in the the authenticator component to animate the default page there. The first step to achieve this is to modify our control group so that we can pass in the show and hide effects as well as the timing (n). Again we will modify the control pair collection so that we can pass these settings in to our control groups. Since these settings will be used by all control groups we will pass them in when constructing our control pair collection (o). Finally we need to create these settings in our authenticator class and pass them into the control pair collection (p).

control-group.ts (3)

...
export interface IControlGroupConfig {
    ...
    hideInfoEffect: AnimationKeyFrame;
    ...
    infoTiming: AnimationEffectTiming;
    showInfoEffect: AnimationKeyFrame;
}
...
export class ControlGroup {
    ...
    private readonly _hideInfoEffect: AnimationKeyFrame = null;
    ...
    private readonly _infoTiming: AnimationEffectTiming = null;
    private readonly _showInfoEffect: AnimationKeyFrame = null;
    ...
    constructor(config: IControlGroupConfig) {
        ...
        this._hideInfoEffect   = config.hideInfoEffect;
        ...
        this._infoTiming       = config.infoTiming;
        this._showInfoEffect   = config.showInfoEffect;
    }
    ...
    public hideInfo = () => {
        return this.infoDOM.animate(this._hideInfoEffect, this._infoTiming);
    }

    public showInfo = () => {
        return this.infoDOM.animate(this._showInfoEffect, this._infoTiming);
    }
}
(n) Instead of defining the animation settings within our control group we will pass them in as a constructor argument.

control-pair-collection.ts (3)

...
export interface IControlPairCollectionConfig {
    ...
    hideInfoEffect: AnimationKeyFrame;
    infoTiming: AnimationEffectTiming;
    showInfoEffect: AnimationKeyFrame;
}

export class ControlPairCollection {
    ...
    private readonly _hideInfoEffect: AnimationKeyFrame = null;
    private readonly _infoTiming: AnimationEffectTiming = null;
    ...
    private readonly _showInfoEffect: AnimationKeyFrame = null;
    ...
    constructor(config: IControlPairCollectionConfig) {
        ...
        this._hideInfoEffect = config.hideInfoEffect;
        this._infoTiming     = config.infoTiming;
        this._showInfoEffect = config.showInfoEffect;
    }
    ...
    public create = (left: ICreatePairConfig, right?: ICreatePairConfig) => {
        const createConfig = (c: ICreatePairConfig) => {
            return {
                domReader: this._domReader,
                elementRef: this._elementRef,
                focusElementType: c.focusElementType,
                hideInfoEffect: this._hideInfoEffect,
                id: c.id,
                infoId: c.infoId,
                infoTiming: this._infoTiming,
                showInfoEffect: this._showInfoEffect
            } as IControlGroupConfig;
        };

        const leftConfig = createConfig(left);
        ...
        const pair = new ControlPair({
            left: leftConfig,
            right: createConfig(right)
        });
        ...
    }
}
(o) Since the animation settings will be passed into each control group we will specify them as an argument when constructing our control pair collection and it will then pass them in when creating a control pair.

authenticator.component.ts (4)

...
export class AuthenticatorComponent implements ... {
    ...
    private readonly _hideInfoEffect: AnimationKeyFrame = {
        color: ["yellow", "#dedede"]
    };
    private readonly _showInfoEffect: AnimationKeyFrame = {
        color: ["#dedede", "yellow"]
    };
    private readonly _infoTiming: AnimationEffectTiming = {
        duration: 350,
        easing: "ease-in-out",
        fill: "forwards"
    };
    ...
    constructor(...) {
        ...
        this._controlPairCollection = new ControlPairCollection({
            ...
            hideInfoEffect: this._hideInfoEffect,
            infoTiming: this._infoTiming,
            showInfoEffect: this._showInfoEffect
        });
        ...
    }
}
(p) We need to define the animation settings and pass them into our control pair collection.

With these changes made we should see no difference in the behaviour of our interface.

Exciton Interactive LLC
Advertisement