#17 Start Animating the Info Pages
Thursday, January 25, 2018
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.
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.
Can never have too many ids
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.ts
-
authenticator
-
components
-
Source
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)
...
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"; }
...
}
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).
We need references to the dom elements
-
WebUi
-
Source
-
components
- authenticator.component.ts
- control-group.ts
- control-pair-collection.ts
-
components
-
Source
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); }
}
}
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
}
});
...
}
}
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
});
}
...
}
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).
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.
Show and hide the correct info page
-
WebUi
-
Source
-
components
- control-group.ts
- control-pair.ts
- control-pair-collection.ts
-
components
-
Source
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"
});
}
}
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.`);
}
}
}
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);
}
}
Did someone mention focus
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.ts
-
authenticator
-
components
-
Source
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}')`)
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]);
}
}
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).
We also need to animate the default info page
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-group.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
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);
}
}
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)
});
...
}
}
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
});
...
}
}
With these changes made we should see no difference in the behaviour of our interface.