#24 Adding the Forgot Link Behaviour
Thursday, March 15, 2018
In this article we focus on adding the behaviour to our interface when the user clicks on either the forgot username or forgot password links. At this point the only thing that happens is our inputs container animates out and back in. What we also need to have happen is the appropriate controls need to be hidden and shown when it animates out so that it is ready to use when it animates back in. We will also add reset functionality so that if the user clicks the reset button all of the controls are set to their original values and returned to a pristine state.
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.
Hiding the forgot links and dynamically changing the button text
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
It's now time to focus some attention on the behaviour of our component when the user clicks one of the forgot links. The first
thing we will do is modify the template (a). If the user clicks on one of the links
we want to hide them as well as hide the remember me control using a few *ngIf
statements.
I have also mentioned in the past that we will set the reset button's text through code and that time has now arrived. To do this we
need to modify our button mixin as well as how it is called. We also want to show a different message if the user click on a forgot
link. Next thing we are going to do is just to set the overflow property of
our authenticator so that when the controls are animated to the left they don't show outside of our container
(b). Finally we need to modify our typescript file
(c). We will start by implementing the method that will return the text for our buttons.
Next if you click on one of the links now you can see the interface updates the title
and hiding of various components at the same time that the container is animated out. This is not the behaviour that we want. We
want all of that updating to happen after the container is animated out and before it is animated back in which we will handle in the
next section.
authenticator.component.pug (1)
...
mixin button(key)
button(..., [innerHtml]=`getButtonText('${key}')`, ...)
...
div.authenticator
div.inputs-container#inputs-container
div.inputs
h3 ...
form(...)
...
div.forgot(*ngIf="isLogin && isSubActionForgotUsername === false && isSubActionForgotPassword === false")
a(...) Forgot your username?
...
div.forgot(*ngIf="isLogin && isSubActionForgotUsername === false && isSubActionForgotPassword === false")
a(...) Forgot your password?
...
div.form-control-group.control-group-remember-me(..., *ngIf="isSubActionForgotPassword === false && isSubActionForgotUsername === false")
...
div.input-buttons
+button("buttonReset")
+button("buttonSend")
div.info-container
div.info-pages
+info(...)
div.info-validator(*ngIf="isLogin && isSubActionForgotPassword === false && isSubActionForgotUsername === false")
...
div.info-validator(*ngIf="isSubActionForgotPassword")
p.validator We are sorry for your inconvenience. Please enter your email address to receive instructions on how to reset your password.
div.info-validator(*ngIf="isSubActionForgotUsername")
p.validator We are sorry for your inconvenience. Please enter your email address to receive your username.
div.info-validator(*ngIf="isRegister")
p.validator We are happy that you have decided to join us. Please enter and submit the required information.
authenticator.component.scss (1)
...
.authenticator {
...
overflow: hidden;
...
}
authenticator.component.ts (2)
...
export class AuthenticatorComponent ... {
...
public getButtonText = (key: string) => {
switch(key) {
case this.buttonSendKey:
return "Send";
case this.buttonResetKey:
return this.isSubActionForgotPassword || this.isSubActionForgotUsername
? "Cancel"
: "Reset";
default:
throw new Error(`Unknown button key: ${key}.`);
}
}
...
}
Waiting until the inputs container is out of view
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
Fortunately for us there is an easy solution for changing when the interface updates in relation to the user clicking on
one of the forgot links. The solutions
is to move the changes to the _isSubActionForgotPassword
and
_isSubActionForgotUsername
fields into the action methods that are passed to the
subActionAnimate
method. But now if you check the animation the interface is not updating
at all. Luckily this fix for this is simple and all we have to do is import the ChangeDetectorRef
from angular and invoke its detectChanges
method after we have made our changes.
authenticator.component.ts (3)
...
export class AuthenticatorComponent ... {
...
public onClickForgotPassword = () => {
this.subActionAnimate(() => {
this._isSubActionForgotPassword = true;
});
}
public onClickForgotUsername = () => {
this.subActionAnimate(() => {
this._isSubActionForgotPassword = true;
});
}
...
private onClickReset = () => {
if (this.isSubActionForgotPassword) {
this.subActionAnimate(() => {
this._isSubActionForgotPassword = false;
});
return;
}
if (this.isSubActionForgotUsername) {
this.subActionAnimate(() => {
this._isSubActionForgotUsername = false;
});
return;
}
}
...
}
Change Detector Ref
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
But now if you check the animation the interface is not updating
at all. Luckily the fix for this is simple and all we have to do is import the ChangeDetectorRef
from angular and invoke its detectChanges
method after we have made our changes.
authenticator.component.ts (4)
...
import { ..., ChangeDetectorRef, ... } from "@angular/core";
...
export class AuthenticatorComponent ... {
...
constructor(..., private readonly _changeDetectorRef: ChangeDetectorRef, ...) {
...
}
...
private subActionAnimate = (action: () => void) => {
...
outAnimation.onfinish = () => {
...
this._changeDetectorRef.detectChanges();
...
};
}
...
}
Adding the forgot link behaviour
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
Since the behaviour is same regardless of whether the user clicks on the forgot username or forgot password links we will start by creating a method that we can call for both (f). We will also create a common method for resetting the interface after the user clicks the reset button.
authenticator.component.ts (5)
...
export class AuthenticatorComponent ... {
...
public onClickForgotPassword = () => {
this.subActionAnimate(() => {
...
this.onClickForgot();
});
}
public onClickForgotUsername = () => {
this.subActionAnimate(() => {
...
this.onClickForgot();
});
}
...
private onClickForgot = () => {
this._controlPairCollection.hideGroup(this.controlUsernameGroupId);
this.controlUsername.updateValueAndValidity({ onlySelf: true });
this._controlPairCollection.hideGroup(this.controlPasswordGroupId);
this.controlPassword.updateValueAndValidity({ onlySelf: true });
this._controlPairCollection.showGroup(this.controlEmailGroupId);
this.controlEmail.updateValueAndValidity({ onlySelf: true });
}
private onClickReset = () => {
if (...) {
this.subActionAnimate(() => {
...
this.onClickResetForgot();
});
return;
}
if (...) {
this.subActionAnimate(() => {
...
this.onClickResetForgot();
});
return;
}
}
private onClickResetForgot = () => {
this._controlPairCollection.showGroup(this.controlUsernameGroupId);
this.controlUsername.updateValueAndValidity({ onlySelf: true });
this._controlPairCollection.showGroup(this.controlPasswordGroupId);
this.controlPassword.updateValueAndValidity({ onlySelf: true });
this._controlPairCollection.hideGroup(this.controlEmailGroupId);
this.controlEmail.updateValueAndValidity({ onlySelf: true });
}
...
}
Preventing auto-focus
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-pair.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
If click on a forgot link now we will see the information pages side of our interface fade in and out a couple of times showing different information. This is happening because we previously set it up so that when a control is shown we automatically focus its input. This auto-focusing is good for when we are animating the controls in response to the user wanting to navigate around the interface but it's not good now. To fix this we are going to setup the ability to override the focusing behaviour of the show group method (g). Next we need to modify the control pair collection as well (h). With those changes made we are now able to use these methods to stop auto-focusing when we show a control group (i).
control-pair.ts (3)
import loDashIsNil = require("lodash/isNil");
...
export class ControlPair {
...
public showGroup = (..., focus?: boolean) => {
...
if (this._activeId === id) {
...
if (loDashIsNil(focus) || focus) {
this.focus(id);
}
return;
}
...
}
...
}
control-pair-collection.ts (1)
...
export class ControlPairCollection {
...
public showGroup = (..., focus?: boolean) => {
this._pairs[id].showGroup(..., focus);
}
...
}
authenticator.component.ts (6)
...
export class AuthenticatorComponent ... {
...
private onClickForgot = () => {
this._controlPairCollection.hideGroup(this.controlUsernameGroupId, false);
...
this._controlPairCollection.hideGroup(this.controlPasswordGroupId, false);
...
this._controlPairCollection.showGroup(this.controlEmailGroupId, false);
...
}
...
private onClickResetForgot = () => {
this._controlPairCollection.showGroup(this.controlUsernameGroupId, false);
...
this._controlPairCollection.showGroup(this.controlPasswordGroupId, false);
...
}
...
}
Resetting the form
-
WebUi
-
Source
-
forms
- form.controller.ts
-
forms
-
Source
This feels like an opportune time to create the ability to reset our form. The first thing we are going to do is
to save the values that we are creating our form controls with as their default values (j).
Once we have these values it's a simple matter of creating a reset method that we can use to call the reset method
from the angular form group and pass in our default values. Lastly we will just modify our
onClickReset
method so that it calls the reset method on our form controller that
we just created.
form.controller.ts (1)
...
export class FormController {
private readonly _defaults: { [key: string]: string|boolean } = {};
...
public create = (config: { [key: string]: [string|boolean, ValidatorFn] }) => {
...
loDashForOwn(config, (v, k) => {
this._defaults[k] = v[0] as string|boolean;
});
console.log(this._defaults);
}
...
public reset = () => {
this.form.reset(this._defaults);
console.log(`email: '${this.getControl("controlEmail").value}'`);
console.log(`email confirm: '${this.getControl("controlEmailConfirm").value}'`);
console.log(`password: '${this.getControl("controlPassword").value}'`);
console.log(`password confirm: '${this.getControl("controlPasswordConfirm").value}'`);
console.log(`remember me: '${this.getControl("controlRememberMe").value}'`);
console.log(`username: '${this.getControl("controlUsername").value}'`);
}
...
}
authenticator.component.ts (7)
...
export class AuthenticatorComponent ... {
...
private onClickReset = () => {
if (this.isSubActionForgotPassword === false && this.isSubActionForgotUsername === false) {
this._formController.reset();
return;
}
...
}
private onClickResetForgot = () => {
this._formController.reset();
...
}
...
}


Now that we are sure that everything is working don't forget to remove all of the console.log
statements.