Advertisement

#24 Adding the Forgot Link Behaviour

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.

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

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.
(a) Time to modify our template in order to respond to the user clicking one of the forgot links.

authenticator.component.scss (1)

...
.authenticator {
    ...
    overflow: hidden;
    ...
}
(b) When we animate the controls we don't want them to be visible outside the border of our authenticator.

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}.`);
        }
    }
    ...
}
(c) We need to update our component so that we can set the button text dynamically.

Waiting until the inputs container is out of view

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

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;
        }
    }
    ...
}
(d) We need to update our component so that we can set the button text dynamically and only update the interface while the controls are not visible.

Change Detector Ref

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

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();
            ...
        };
    }
    ...
}
(e) We need to use the change detector ref to let angular know that our interface needs to be updated.
Advertisement

Adding the forgot link behaviour

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

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 });
    }
    ...
}
(f) Here we are creating a common method for when the user clicks a forgot link and also if they click the reset button.

Preventing auto-focus

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

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;
        }
        ...
    }
    ...
}
(g) Adding the ability for us to prevent the auto-focusing of a control group when it is shown.

control-pair-collection.ts (1)

...
export class ControlPairCollection {
    ...
    public showGroup = (..., focus?: boolean) => {
        this._pairs[id].showGroup(..., focus);
    }
    ...
}
(h) We have to modify the method in the control pair collection in order to allow us to stop the focusing behaviour.

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);
        ...
    }
    ...
}
(i) Now that we can prevent auto-focusing we will do so for each show group method call that we make. This will prevent the flashing of the info pages.

Resetting the form

  • WebUi
    • Source
      • forms
        • form.controller.ts

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}'`);
    }
    ...
}
(j) Adding the ability for our form controller to remember what the default value for our form controls are and apply it to them when the reset method is called.

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();
        ...
    }
    ...
}
(k) Calling the reset method of the form controller when the reset button is clicked.
Console output showing that we are correctly creating
            our default values object.
(l) Console out put showing that we are correctly creating our default values object.
Console output showing that we are applying the default values to
            our controls when the form is reset.
(m) Console output showing that we are applying the default values to our controls when the form is reset.

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

Exciton Interactive LLC
Advertisement