#22 Start Animating the Control Groups
Thursday, March 1, 2018
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.
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.
Change to styling
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
-
authenticator
-
components
-
Source
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;
}
}
Exit stage left
-
WebUi
-
Source
-
components
-
authenticator
- control-group.ts
- control-pair.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
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"
});
}
...
}
control-pair.ts (1)
...
export class ControlPair {
...
public hideGroupToLeft = (id: string) => {
this.find(id).hideGroupToLeft();
}
...
}
control-pair-collection.ts (1)
...
export class ControlPairCollection {
...
public hideGroupToLeft = (id: string) => {
this._pairs[id].hideGroupToLeft(id);
}
...
}
Easier to test if we can see it
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
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;*/
}
...
}
...
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
public ngAfterViewInit(): void {
...
this._controlPairCollection.hideGroupToLeft(this.controlPasswordGroupId);
...
}
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.
Will they ever have the same behaviour?
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
-
authenticator
-
components
-
Source
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;
...
}
...
}
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.
Now exit stage right
-
WebUi
-
Source
-
components
-
authenticator
- control-group.ts
- control-pair.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
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"
});
}
}
control-pair.ts (2)
...
export class ControlPair {
...
public hideGroupToRight = (id: string) => {
this.find(id).hideGroupToRight();
}
...
}
control-pair-collection.ts (2)
...
export class ControlPairCollection {
...
public hideGroupToRight = (id: string) => {
this._pairs[id].hideGroupToRight(id);
}
...
}
What do they look like hidden to the right
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
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]);
...
}
authenticator.component.scss (4)
...
.authenticator {
...
.form-control-group {
...
label {
...
white-space: nowrap;
}
}
...
}
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).
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
-
authenticator
-
components
-
Source
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);
}
}
control-pair.ts (3)
...
export class ControlPair {
...
public showGroupFromLeft = (id: string) => {
this.find(id).showGroupFromLeft();
}
public showGroupFromRight = (id: string) => {
this.find(id).showGroupFromRight();
}
...
}
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);
}
...
}
Button click animation
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
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}')`)
...
...
authenticator.component.scss (5)
...
.authenticator {
...
.inputs-container {
...
overflow: hidden;
...
}
...
}
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;
}
}
...
}
Cleanup aisle 9
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
- control-group.ts
-
authenticator
-
components
-
Source
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],
...
},
...);
}
}
authenticator.component.scss (6)
...
.authenticator {
...
.hidden-group {
display: none; // <- Remove comments
}
...
}