#23 Finish Animating the Control Groups
Thursday, March 8, 2018
In this article we will finish the process of animating our control groups. This will require us to define and then implement the control flow that we want when the user clicks on the group buttons or uses the keyboard to navigate our interface.
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.
Simplifying hiding, showing and more
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-group.ts
- control-pair.ts
-
authenticator
-
components
-
Source
Now that we are able to animate the hiding of our control groups we also need to apply the hidden-group
class
to them once the animation has completed. To do this we just need to change our hideGroupToLeftOrRight
method in the control-group.ts file so that it returns the animation and set the onfinish
function for it in both the
hideGroupToLeft
and hideGroupToRight
methods to call the
hideGroup
method (a). While we are making changes to the hide
methods we can also make some changes to our show methods to make them easier to use. The first thing we can do is to call the
showGroup
method before running an animation. We can also return the animation from the
showGroupFromLeftOrRight
method so that we can focus the input once the animation is finished.
Next up is modifying our control-pair.ts class so that when we call the showGroup
method it runs
the appropriate animation for us. We will do this by simple comparing the control that we find using the id that is passed in to the left
and right controls of the pair. If it is equal to the left then we will show the left control from the left and hide the right control to
the right and vise versa for showing the right control (b). With those changes made we can simplify the
onClickGroupButton
method located within the authenticator.component.ts file
(c).
control-group.ts (1)
...
export class ControlGroup {
...
public hideGroupToLeft = () => {
...
const animation = this.hideGroupToLeftOrRight(...);
animation.onfinish = () => {
this.hideGroup();
};
}
public hideGroupToRight = () => {
...
const animation = this.hideGroupToLeftOrRight(...);
animation.onfinish = () => {
this.hideGroup();
};
}
...
public showGroupFromLeft = () => {
this.showGroup();
...
const animation = this.showGroupFromLeftOrRight(...);
animation.onfinish = () => {
this.focus();
};
}
public showGroupFromRight = () => {
this.showGroup();
...
const animation = this.showGroupFromLeftOrRight(...);
animation.onfinish = () => {
this.focus();
};
}
...
private hideGroupToLeftOrRight = (...) => {
return this.groupDOM.animate(...);
}
private showGroupFromLeftOrRight = (...) => {
return this.groupDOM.animate(...);
}
}
control-pair.ts (1)
...
export class ControlPair {
...
public showGroup = (id: string) => {
const group = this.find(id);
if (group === this._left) {
group.showGroupFromLeft();
this._right.hideGroupToRight();
} else {
group.showGroupFromRight();
this._left.hideGroupToLeft();
}
}
...
}
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
public onClickGroupButton = (key: string) => {
switch(key) {
case this.controlEmailKey:
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
break;
case this.controlEmailConfirmKey:
this._controlPairCollection.showGroup(this.controlEmailGroupId);
break;
case this.controlPasswordKey:
this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
break;
case this.controlPasswordConfirmKey:
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
break;
}
}
...
}
Which control is next or previous?
The next thing that we will tackle is being able to determine which control should be displayed independent of whether or not the user clicks the button on uses the keyboard. Image (d) shows the flow between controls if the user is logging in and image (e) shows the flow if the user is registering. In both images the starting point is the username control at the top and ending point is the buttons at the bottom. If they are using the keyboard to navigate the path that should be followed if they are holding down the alt key should be the same except in reverse.
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-pair.ts
-
authenticator
-
components
-
Source
Since we need to handle both the clicking of the input group button and keyboard navigation what we need to do then is create a couple of helper methods that we can use to determine the next and previous controls (f). The next issue we want to deal with is if the control we are navigating to is already visible we do not want the show animation running. To prevent it from running we are simple going to introduce a field that will keep track of which control is visible in the control pair (g).
authenticator.component.ts (2)
...
export class AuthenticatorComponent ... {
...
public onClickGroupButton = (key: string) => {
this.nextControl(key);
}
...
private nextControl = (key: string) => {
switch (key) {
case this.controlUsernameKey:
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
break;
case this.controlPasswordKey:
if (this.isLogin) {
this._controlPairCollection.focus(this.controlRememberMeGroupId);
} else if (this.isRegister) {
this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
}
break;
case this.controlPasswordConfirmKey:
this._controlPairCollection.showGroup(this.controlEmailGroupId);
break;
case this.controlEmailKey:
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
break;
case this.controlEmailConfirmKey:
if (this.authForm.valid) {
this.buttonSendDOM.focus();
} else {
this.buttonResetDOM.focus();
}
break;
case this.controlRememberMeKey:
if (this.authForm.valid) {
this.buttonSendDOM.focus();
} else {
this.buttonResetDOM.focus();
}
break;
}
}
private previousControl = (key: string) => {
switch (key) {
case this.controlPasswordKey:
this._controlPairCollection.showGroup(this.controlUsernameGroupId);
break;
case this.controlPasswordConfirmKey:
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
break;
case this.controlEmailKey:
this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
break;
case this.controlEmailConfirmKey:
this._controlPairCollection.showGroup(this.controlEmailGroupId);
break;
case this.controlRememberMeKey:
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
break;
case this.buttonResetKey:
if (this.isLogin) {
this._controlPairCollection.focus(this.controlRememberMeGroupId);
} else if (this.isRegister) {
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
}
break;
case this.buttonSendKey:
this.buttonResetDOM.focus();
break;
}
}
...
private tabOrEnter = (event: KeyboardEvent, key: string) => {
let preventDefault = true;
switch(key) {
case this.controlUsernameKey:
if (event.shiftKey) {
preventDefault = false;
} else {
this.nextControl(key);
}
break;
case this.buttonResetKey:
case this.buttonSendKey:
if (event.shiftKey) {
this.previousControl(key);
} else {
preventDefault = false;
}
break;
default:
if (event.shiftKey) {
this.previousControl(key);
} else {
this.nextControl(key);
}
break;
}
return preventDefault;
}
...
}
control-pair.ts (2)
...
export class ControlPair {
...
private _activeId: string = null;
...
constructor(config: IControlPairConfig) {
...
this._activeId = this._left.id;
}
...
public showGroup = (id: string) => {
...
if (this._activeId === id) {
group.showGroup();
this.focus(id);
return;
}
this._activeId = id;
...
}
}
Too bad the control flow isn't that simple
Unfortunately the control flow that we have created is only the behaviour we want if all of the controls are in a valid state and the user is using the keyboard to navigate. In the following images the green arrows represent the flow if the control is valid and the red if it is invalid.
Keyboard
Image (h) shows the flow for logging in and image (i) shows the flow for registering. One important consideration we have for keyboard navigation is we want there to always be a path to the reset button.
Button
In both (j) and (k) a red arrow that starts and ends on the same control simply means that we will refocus the control if the user presses the button and the control is invalid. Unlike when the user is using the keyboard to navigate we no longer need to have a path to the reset button if the user is clicking on the group buttons. Another important change is that if the user presses one of the buttons belonging to a confirm group then we will animate back to the first control whether or not the confirm control is valid. This behaviour is represented by the orange arrows.
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
To achieve the behaviour we discussed above we will start by modifying the onClickGroupButton
method.
If the key belongs to one of the confirm controls then we just navigate to the previous control regardless of whether the current control is
valid or not. Otherwise if it is invalid we just refocus it and if it is valid we move to the next control. We are also making a little helper
method to pick which button should be focused based on whether the form is valid or not. Lastly we need to modify the
nextControl
and previousControl
methods to account for the
flows shown in the previous images.
authenticator.component.ts (3)
...
export class AuthenticatorComponent ... {
...
public onClickGroupButton = (key: string) => {
switch(key) {
case this.controlPasswordConfirmKey:
case this.controlEmailConfirmKey:
this.previousControl(key);
break;
default:
const control = this[key] as FormControl;
if (control.invalid) {
this._controlPairCollection.focus(this._keyToGroupIdMap[key]);
return;
}
this.nextControl(key);
break;
}
}
...
private focusButton = () => {
if (this.authForm.valid) {
this.buttonSendDOM.focus();
} else {
this.buttonResetDOM.focus();
}
}
...
private nextControl = (key: string) => {
switch (key) {
...
case this.controlPasswordKey:
if (...) {
...
} else if (...) {
if (this.controlPassword.valid) {
this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
} else {
this._controlPairCollection.showGroup(this.controlEmailGroupId);
}
}
break;
case this.controlPasswordConfirmKey:
if (this.controlPasswordConfirm.valid) {
this._controlPairCollection.showGroup(this.controlEmailGroupId);
} else {
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
}
break;
case this.controlEmailKey:
if (this.controlEmail.valid) {
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
} else {
this.focusButton();
}
break;
case this.controlEmailConfirmKey:
if (this.controlEmailConfirm.valid) {
this.focusButton();
} else {
this._controlPairCollection.showGroup(this.controlEmailGroupId);
}
break;
case this.controlRememberMeKey:
this.focusButton();
break;
}
}
private previousControl = (key: string) => {
switch (key) {
...
case this.controlEmailKey:
if (this.controlPassword.valid) {
this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
} else {
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
}
break;
...
case this.buttonResetKey:
if (...) {
...
} else if (...) {
if (this.controlEmail.valid) {
this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
} else {
this._controlPairCollection.showGroup(this.controlEmailGroupId);
}
}
break;
...
}
}
...
}