Advertisement

#22 Start Animating the Control Groups

In this article we will truly begin the processing of animating our control groups. By the time we are done with this article we will have defined the animation keyframes for both showing and hiding the groups from both the left and right of the inputs container. We will also have begun writing the code that will cause the animations to play in response to input from the user. We will then finish up the animation process in the following article.

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

Change to styling

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.scss

I want to begin by adjusting the way I handle changes to our sass files. Up until now I have been making changes to our sass files by just adding to them and not updating what has been previously added. For a few reasons this has annoyed me enough for me to decide that from now on I will update what has already been written, when possible, and adding when not. To facilitate this change I have condensed the contents of the authenticator.component.scss file (a) which you can use to replace the code of your current file.

authenticator.component.scss (1)

@import "../../sass/_bourbon-neat.scss";

@import "../../sass/2-base/_media-queries.scss";

$base-font-size: 16px;

$info-container-background-color: #484848;
$info-container-color: #dedede;
$info-heading-background-color: darken($info-container-background-color, 2%);

$awaiting-confirmation-color: #E87E04 !default;
$indeterminate-color: #BDC3C7 !default;
$invalid-color: #D91E18 !default;
$valid-color: #26A65B !default;

@function em($pixel) {
    @return #{strip-unit($pixel)/strip-unit($base-font-size)}em;
}

@mixin inputGroup($color) {
    input, button {
        border-color: $color;

        &:focus {
            outline: none !important;
            box-shadow: 0 0 em(10px) $color;
        }
    }

    button {
        background-color: $color;
        opacity: 1;
    }
}

::-webkit-scrollbar {
    width: 16px;
    background-color: #292929;
}

::-webkit-scrollbar-track {
    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
    border-radius: 0;
    background-color: #292929;
}

::-webkit-scrollbar-thumb {
    border-radius: 0;
    -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
    background-color: #484848;
}

.authenticator {
    background-color: #1b1b1b;
    font-size: $base-font-size;
    background-color: white;
    margin: 0 auto;
    border: 1px solid $info-container-background-color;

    @include media-gt-or-eq(640px) {
        display: flex;
        flex-direction: row;
        width: em(640px);

        .inputs-container, .info-container {
            width: em(320px);
        }
    }

    a {
        text-decoration: underline;
        text-decoration-style: ink;
        color: #2593cc;
        cursor: pointer;
    }

    .form-control-group {
        label {
            font-weight: 600;
        }
    }

    .inputs-container {
        min-height: 383px;
        display: flex;
        flex-direction: column;
        background-color: white;
        padding: em(10px);

        .inputs {
            flex: 1;
        }
    }

    .hidden-group {
        display: none;
    }

    .input-group {
        display: flex;
        flex-direction: row;
        margin-bottom: em(12px);

        &.indeterminate {
            @include inputGroup($indeterminate-color);
        }

        &.invalid {
            @include inputGroup($invalid-color);
        }

        &.valid {
            @include inputGroup($valid-color);
        }

        &.awaiting-confirmation {
            @include inputGroup($awaiting-confirmation-color);

            @keyframes awaiting-confirmation-btn {
                0% {
                    background-color: $awaiting-confirmation-color;
                }

                50% {
                    background-color: lighten($awaiting-confirmation-color, 10);
                }

                100% {
                    background-color: $awaiting-confirmation-color;
                }
            }

            button {
                animation-name: awaiting-confirmation-btn;
                animation-iteration-count: infinite;
                animation-duration: 1.5s;
            }
        }

        input, button {
            border-bottom: em(2px) solid;
            transition: all 1s;
            margin-bottom: 0;
        }

        input {
            margin-bottom: 0;
            min-width: 0;
        }

        button {
            padding: em(12px) em(16px);
            color: white;
            line-height: 1;
        }
    }

    .dual-input-group {
        display: flex;
        flex-direction: row;

        .form-control-group {
            flex: 1;
        }
    }

    .forgot {
        text-align: right;
    }

    .control-group-remember-me {
        input[type="checkbox"] {
            display: none;

            + label {
                margin: em(10px) 0;

                &:focus {
                    outline: none !important;
                    box-shadow: 0 0 em(10px) #BDC3C7;
                }
            }
        }

        input[type="checkbox"] + label span {
            display: inline-block;
            width: em(19px);
            height: em(19px);
            margin: em(-1px) em(4px) 0 0;
            vertical-align: middle;
            background-color: white;
            cursor: pointer;
            position: relative;
            border: em(1px) solid $info-container-color;

            i {
                position: absolute;
                left: em(1px);
                top: 0;
            }
        }

        input[type="checkbox"]:checked + label span {
            i:before {
                font-family: FontAwesome;
                content: "\f00c";
            }
        }
    }

    .input-buttons {
        display: flex;
        flex-direction: row;

        button {
            flex: 1;
            background-color: $info-container-background-color;

            &:first-child {
                border-right: 1px solid white;
            }
        }
    }

    .info-container {
        display: flex;
        flex-direction: column;
        color: $info-container-color;
        background-color: $info-container-background-color;
    }

    .need-to-login, .need-to-register {
        text-align: right;
        padding: em(10px);

        a {
            color: $info-container-color;
        }
    }

    .info-heading {
        padding: em(10px);
        background-color: $info-heading-background-color;
        border-bottom: 1px solid #3b3b3b;
        margin-bottom: 0;
    }

    .info-validator {
        display: flex;
        flex-direction: row;
        margin-bottom: em(4px);
        padding: em(5px) em(10px);

        &:nth-child(2n) {
            background-color: $info-heading-background-color;
        }

        .validator {
            margin-bottom: 0;
            flex: 1;
        }

        .validator-icon {
            padding-top: em(2px);
            transition: all 0.5s;

            &.fa-circle-o-notch {
                color: $indeterminate-color;
            }

            &.fa-check {
                color: $valid-color;
            }

            &.fa-times {
                color: $invalid-color;
            }
        }
    }

    .info-pages {
        position: relative;
        overflow-y: auto;
        flex: 1;

        .info {
            position: absolute;
            opacity: 0;
            left: 0;
            right: 0;
        }
    }

    .model-errors {
        border: 1px solid black;
        border-left-width: 0;
        border-right-width: 0;
        color: $invalid-color;
        padding: em(10px);
        background-color: #2b2b2b;
        list-style-type: disc;
        padding-left: em(30px);
    }

    .hidden-info {
        display: none;
    }
}
(a) Condensed styling for our authenticator component. Please copy this code and replace the contents of your authenticator scss file with it.

Exit stage left

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

To start animating our controls we will need to be able to hide some of them by animating them off to the left. To do this we will start by creating a method on our control group class (b). The animation is performed by translating the group's DOM in the negative x direction by an amount equal to its width. Once that is done we will just need to add an appropriate method to our control pair (c) and control pair collection (d).

control-group.ts (1)

...
export class ControlGroup {
    ...
    public hideGroupToLeft = () => {
        const width = `${this.groupDOM.clientWidth}px`;
        const left = ["translate(0)", `translate(-${width})`] as [string, string];
        this.groupDOM.animate(
            {
                flex: [1, 0],
                transform: left,
                width: [width, 0]
            },
            {
                duration: 350,
                easing: "ease-in-out",
                fill: "forwards"
            });
    }
    ...
}
(b) The first method that we can use to animate our control groups will cause them to be hidden by animating to the left of the inputs container.

control-pair.ts (1)

...
export class ControlPair {
    ...
    public hideGroupToLeft = (id: string) => {
        this.find(id).hideGroupToLeft();
    }
    ...
}
(c) To use the new method we need to create a corresponding method in the control pair class.

control-pair-collection.ts (1)

...
export class ControlPairCollection {
    ...
    public hideGroupToLeft = (id: string) => {
        this._pairs[id].hideGroupToLeft(id);
    }
    ...
}
(d) And a corresponding method in the control pair collection class.

Easier to test if we can see it

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

I know that it is kind of ironic that we spent the last video creating the ability for us to hide the control groups and one of the first steps we are going to do now that we are starting the animation process is to disable that ability. This is of course only temporary but makes it a lot easier for us to test what is happening when the controls are animating. With that being said we want to start by modifying the scss file to prevent a hidden group from actually being hidden (e). Next we need to test our hideGroupToLeft method by adding a call to it at the bottom of the ngAfterViewInit method (f).

authenticator.component.scss (2)

...
.authenticator {
    ...
    .hidden-group {
        /*display: none;*/
    }
    ...
}
...
(e) Commenting out the display setting for the hidden group class so that we can more easily test our animations.

authenticator.component.ts (1)

...
export class AuthenticatorComponent ... {
    ...
    public ngAfterViewInit(): void {
        ...
        this._controlPairCollection.hideGroupToLeft(this.controlPasswordGroupId);
    ...
}
(f) In order to test our hide to left animation we will make a call to it after angular has initialized our view.

When those changes have been updated by webpack we will see at least two different layouts (I don't currently have any apple devices so I can't test it in Safari) for our component depending on what browser you are using. In (g) we wee our interface as it is displayed in Chrome and in (h) as it is displayed in Firefox.

Our interface being displayed in Chrome with a control group hidden to the left.
(g) Our interface being displayed in Chrome with a control group hidden to the left.
Our interface being display in Firefox with a control group hidden to the left.
(h) Our interface being display in Firefox with a control group hidden to the left.

Will they ever have the same behaviour?

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.scss

Fortunately for us the change that we need to make to have both Chrome and Firefox behave the same is extremely simple. All that we have to do is adjust the min width of our form control groups (i).

authenticator.component.scss (3)

...
.authenticator {
    ...
    .form-control-group {
        min-width: 0;
        ...
    }
    ...
}
(i) To fix the display problem in Firefox we need to change the min width to zero.

With that small change done we now see that Chrome (j) and Firefox (k) both display the same interface when a control group is hidden.

With the change to the minimum width Chrome still displays the interface the same way that it did previously.
(j) With the change to the minimum width Chrome still displays the interface the same way that it did previously.
Now Firefox displays the interface the same way that Chrome does.
(k) Now Firefox displays the interface the same way that Chrome does.
Advertisement

Now exit stage right

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

Now that we can hide control groups to the left let's add the ability to hide them to the right and at the same time refactor our code. The process of hiding control groups either to the left or to the right has a few properties in common. To take advantage of this we will create a helper function that both will call in order to perform the animation (l). With that done it has become almost automatic that we need to add a method to both our control pair (m) and control pair collection (n) that we can use to hide the control group to the right.

control-group.ts (2)

...
export class ControlGroup {
    ...
    public get groupClientWidth() {
        return `${this.groupDOM.clientWidth}px`;
    }
    ...
    public hideGroupToLeft = () => {
        const left = ["translate(0)", `translate(-${this.groupClientWidth})`] as [string, string];
        this.hideGroupToLeftOrRight(left);
    }

    public hideGroupToRight = () => {
        const right = ["translate(0)", `translate(${this.groupClientWidth})`] as [string, string];
        this.hideGroupToLeftOrRight(right);
    }
    ...
    private hideGroupToLeftOrRight = (transform: [string, string]) => {
        this.groupDOM.animate(
            {
                flex: [1, 0],
                transform: transform,
                width: [this.groupClientWidth, 0]
            },
            {
                duration: 350,
                easing: "ease-in-out",
                fill: "forwards"
            });
    }
}
(l) Time to add the ability to hide a group to the right. Since both hiding to the left and right have very similar code we will create a helper method that we can call for both.

control-pair.ts (2)

...
export class ControlPair {
    ...
    public hideGroupToRight = (id: string) => {
        this.find(id).hideGroupToRight();
    }
    ...
}
(m) Just need to add a method to the control pair to hide a group to the right.

control-pair-collection.ts (2)

...
export class ControlPairCollection {
    ...
    public hideGroupToRight = (id: string) => {
        this._pairs[id].hideGroupToRight(id);
    }
    ...
}
(n) Last but not least we need to add a method the collection as well.

What do they look like hidden to the right

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

What do you think we are going to do next? If you said test out hiding a control group to the right you would be 100% correct. We will return to our after view init method and add a statement to hide the email confirm control to the right (o).

authenticator.component.ts (2)

...
export class AuthenticatorComponent ... {
    ...
    public ngAfterViewInit(): void {
        ...
        this._controlPairCollection.hideGroupToRight(this._keyToGroupIdMap[this.controlEmailConfirmKey]);
    ...
}
(o) Time to test hiding a control group to the right.

authenticator.component.scss (4)

...
.authenticator {
    ...
    .form-control-group {
        ...
        label {
            ...
            white-space: nowrap;
        }
    }
    ...
}
(p) We need to prevent the label from wrapping when the width of the control group is reduced to zero.

Before making the change in (p) we can see that the label in our control group is wrapping to a second line (q). This would not be an issue if it did not also cause the height of our component to change. Since it does we just need to add the nowrap style which corrects our issue (r).

The label of the control group wraps to a second line when the width
            of the control group is reduced to zero.
(q) The label of the control group wraps to a second line when the width of the control group is reduced to zero.
We just need to add a simple change to our styling to prevent the wrapping which in turn
            prevents the height of our component from changing.
(r) We just need to add a simple change to our styling to prevent the wrapping which in turn prevents the height of our component from changing.

At this point our ad hoc hiding of control groups has served its purpose so it's time to remove the hiding statements from our after view init method.

We can't just hide everything

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

It's not very helpful if all we can do is hide control groups so now it's time to add the ability to show them as well. Now that we have the hang of it we will add both the show from left and right methods at the same time (s). While we are doing this we will of course refactor our code a little bit at the same time. By now I know that I don't have to mention it but we also need to add the show methods to our control pair (t) and control pair collection (u) classes.

control-group.ts (3)

...
export class ControlGroup {
    ...
    private readonly _toFromTiming: AnimationEffectTiming = {
        duration: 350,
        easing: "ease-in-out",
        fill: "forwards" 
    };
    ...
    public showGroupFromLeft = () => {
        const right = [`translate(-${this.groupClientWidth})`, "translate(0)"] as [string, string];
        this.showGroupFromLeftOrRight(right);
    }

    public showGroupFromRight = () => {
        const left = [`translate(${this.groupClientWidth})`, "translate(0)"] as [string, string];
        this.showGroupFromLeftOrRight(left);
    }
    ...
    private hideGroupToLeftOrRight = (...) => {
        return this.groupDOM.animate(
            ...,
            this._toFromTiming);
    }

    private showGroupFromLeftOrRight = (transform: [string, string]) => {
        this.groupDOM.animate({
            flex: [0, 1],
            transform: transform,
            width: [0, this.groupClientWidth]
        }, this._toFromTiming);
    }
}
(s) It's time to add the ability to show our control groups in addition to being able to hide them.

control-pair.ts (3)

...
export class ControlPair {
    ...
    public showGroupFromLeft = (id: string) => {
        this.find(id).showGroupFromLeft();
    }

    public showGroupFromRight = (id: string) => {
        this.find(id).showGroupFromRight();
    }
    ...
}
(t) As usual we need to add methods to our control pair.

control-pair-collection.ts (3)

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

    public showGroupFromRight = (id: string) => {
        this._pairs[id].showGroupFromRight(id);
    }
    ...
}
(u) And we have to add methods to our control pair collection as well.

Button click animation

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

It's time to use what have built here to animate our control groups in response to the user clicking on the control group's button. The first step is to modify our template to bind the click event to an event handler that we will be adding shortly (v). Before we add the event handler it's time to modify our styles so that our inputs container prevents the displaying of any content outside its borders (w). Now we turn to the creation of our click event handler (x). Our first iteration of the animation is simple to toggle between the control group that is being displayed when we press on a button contained within a control pair.

authenticator.component.pug (1)

...
mixin formControlGroup(...)
    div.form-control-group(...)
        label #{label}
        div.input-group(...)
            input(...)
            button(..., (click)=`onClickGroupButton('${key}')`)
                ...
...
(v) We have to modify our template so that we can respond to the user clicking the control group button.

authenticator.component.scss (5)

...
.authenticator {
    ...
    .inputs-container {
        ...
        overflow: hidden;
        ...
    }
    ...
}
(w) Modifying the styling of the input container to prevent displaying any content outside of its borders.

authenticator.component.ts (3)

...
export class AuthenticatorComponent ... {
    ...
    public onClickGroupButton = (key: string) => {
        switch(key) {
            case this.controlEmailKey:
                this._controlPairCollection.hideGroupToLeft(this._keyToGroupIdMap[key]);
                this._controlPairCollection.showGroupFromRight(this.controlEmailConfirmGroupId);
                break;
            case this.controlEmailConfirmKey:
                this._controlPairCollection.showGroupFromLeft(this.controlEmailGroupId);
                this._controlPairCollection.hideGroupToRight(this._keyToGroupIdMap[key]);
                break;
            case this.controlPasswordKey:
                this._controlPairCollection.hideGroupToLeft(this._keyToGroupIdMap[key]);
                this._controlPairCollection.showGroupFromRight(this.controlPasswordConfirmGroupId);
                break;
            case this.controlPasswordConfirmKey:
                this._controlPairCollection.showGroupFromLeft(this.controlPasswordGroupId);
                this._controlPairCollection.hideGroupToRight(this._keyToGroupIdMap[key]);
                break;
        }
    }
    ...
}
(x) Simple event handler that toggles displaying a control group between the pair of control groups.

Cleanup aisle 9

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.scss
          • control-group.ts

The last thing that we are going to do in this article is a couple of clean up changes. First if we check the animation now we will see there is some overlap between the control group that is animating out the the group animating in. We are going to deal with this by modifying the show and hide keyframes to include adjustment to the opacity (y). Lastly we just need to remove the comments around the display: none; statement for the hidden group class in our scss file (z).

control-group.ts (4)

...
export class ControlGroup {
    ...
    private hideGroupToLeftOrRight = (...) => {
        this.groupDOM.animate(
            {
                ...
                opacity: [1, 0],
                ...
            },
            ...);
    }

    private showGroupFromLeftOrRight = (...) => {
        this.groupDOM.animate(
            {
                ...
                opacity: [0, 1],
                ...
            },
            ...);
    }
}
(y) Small tweak to the show and hide animation keyframs in order to animate the opacity.

authenticator.component.scss (6)

...
.authenticator {
    ...
    .hidden-group {
        display: none; // <- Remove comments
    }
    ...
}
(z) Removing the comments preventing the display being set to none for control groups that have the hidden-group class applied to them.
Exciton Interactive LLC
Advertisement