#21 Hiding the Control Groups
Thursday, February 22, 2018
In this article we will focus on adding the ability to hide control groups when we wish to but during the process we will identify an issue with our input pages which will require us to modify how we are hiding them as well. We will also hide the forgot links when the user is registering, add a little bit of styling and finsih up with adding the ability to reset our form when the user presses the reset button.
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.
Setting up the ability to hide groups
-
WebUi
-
Source
-
components
-
authenticator
- control-group.ts
- control-pair.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
We will start as we have before by creating methods to hide and show our control groups (a) and propagating that ability up the chain. That means after adding the methods to the control groups we will add them to the control pairs (b) and finally to the control pair collection (c). Our approach to hiding the control groups is to add a css class using the classlist api and then removing that class to show the groups.
control-group.ts (1)
...
export class ControlGroup {
...
private get hiddenGroupClass() {
return "hidden-group";
}
...
public hideGroup = () => {
this.groupDOM.classList.add(this.hiddenGroupClass);
}
...
public showGroup = () => {
this.groupDOM.classList.remove(this.hiddenGroupClass);
}
}
control-pair.ts (1)
...
export class ControlPair {
...
public hideGroup = (id: string) => {
this.find(id).hideGroup();
}
...
public showGroup = (id: string) => {
this.find(id).showGroup();
}
...
}
control-pair-collection.ts (1)
export class ControlPairCollection {
...
public hideGroup = (id: string) => {
this._pairs[id].hideGroup(id);
}
...
public showGroup = (id: string) => {
this._pairs[id].showGroup(id);
}
...
}
Actually hiding a control group
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
Now that we can add/remove the 'hidden-group' css class to/from our control groups we need to provide the styling for that class. It is probably pretty easy to guess what that styling is going to be. We are simple going to set the display to none as shown in (d). To see that everything is working the way we intend it to we can just test it by hiding the password confirm control group (e).
authenticator.component.scss (1)
.authenticator {
.hidden-group {
display: none;
}
}
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
public ngAfterViewInit(): void {
...
this._controlPairCollection.hideGroup(this._keyToGroupIdMap[this.controlPasswordConfirmKey]);
}
}
We can see in (f) that the password confirm control group does in fact have the 'hidden-group' class applied to it which results in it being hidden in (g).
Now that we are sure it is working we need to remove the
this._controlPairCollection.hideGroup(this._keyToGroupIdMap[this.controlPasswordConfirmKey]);
statement from our code.
Hide control groups when they are instantiated
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-group.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
We do have several control groups that we would like to have hidden by default. For example whether the user is attempting to login or register we would like both confirm groups to be hidden to begin with. We would also like the email group to be hidden, again at the start, if the user is attempting to login. To accomplish this we start by defining a parameter on the config object for creating a control group which is an optional boolean (h). If this boolean is defined and set to true we will invoke the hide method when the group is created. In order to be able to pass this parameter into our control group constructors we also need to modify the control pair collection (i). With these changes made we can now specify whether a control group should be hidden from the start (j).
control-group.ts (2)
import loDashIsNil = require("lodash/isNil");
...
export interface IControlGroupConfig {
...
hideGroup?: boolean;
...
}
export class ControlGroup {
...
constructor(...) {
...
if (config.hideGroup) {
this.hideGroup();
}
}
...
}
control-pair-collection.ts (2)
...
export interface ICreatePairConfig {
...
hideGroup?: boolean;
...
}
...
export class ControlPairCollection {
...
public create = (...) => {
const createConfig = (c: ICreatePairConfig) => {
return {
...
hideGroup: c.hideGroup,
...
} ...
};
...
}
...
}
authenticator.component.ts (2)
export class AuthenticatorComponent ... {
...
public ngAfterViewInit(): void {
this._controlPairCollection.create(
{ // Email
hideGroup: this.isLogin,
...
},
{ // Email Confirm
hideGroup: true,
...
});
this._controlPairCollection.create(
{ // Password
...
},
{ // Password Confirm
hideGroup: true,
...
});
this._controlPairCollection.create(
{ // Remember Me
...
hideGroup: this.isRegister,
...
});
...
}
...
}
Now that we are specifying which groups should be hidden when they are constructed the ui when the user is attempting to login should look like (k) and if they are attempting to register it should look like (l).
A problem lurking in the background
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.scss
-
authenticator
-
components
-
Source
The next thing we want to do is to remove the forgot urls from the interface if the user is registering. This is of course a very
simple thing to do by using a couple of *ngIf
statements within the template
(m). I would also like to have some separation between the last control and the
reset and send buttons so we need to put the control groups in their own container so that we can expand it to fill the
available space thereby pushing the buttons down. In order to create the separation we will also set a minimum height to the
inputs container and while we are at it we set the info pages overflow property and style the scrollbars. Although I really
do like the styling of the scrollbars I should mention that this will only work on webkit browsers for now.
authenticator.component.pug (1)
...
div.authenticator
div.inputs-container#inputs-container
...
form.inputs(...)
...
div.forgot(*ngIf="isLogin")
...
...
div.forgot(*ngIf="isLogin")
...
...
div.input-buttons
...
authenticator.component.scss (2)
::-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 {
.inputs-container {
min-height: 383px;
display: flex;
flex-direction: column;
.inputs {
flex: 1;
}
}
.info-pages {
overflow-y: auto;
}
}
With the scrollbars being shown now we can see that we have a bit of problem. The problem is we are just setting the opacity to zero for hidden info pages so although we cannot see them they are still influencing the way the DOM is being displayed. Although we can see it now the problem already existed when the changes that we made resulted in the height of the ui getting smaller, especially for logging in. In that case the 'hidden' info pages were already overlapping the need to register an account link making it impossible to click it. We will fix these problems in the next section.
Hidden info pages should actually be hidden
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
- authenticator.component.ts
- control-group.ts
-
authenticator
-
components
-
Source
We have already talked about the problem being we are only setting the opacity to 0 when hiding an info page. Our solution is of course to do the same thing that we did with the control groups and add/remove a css class that we can use to change a page's display to none when appropriate. This is of course complicated a little bit by the fact that we would like the hide animation to finish before adding this class. We start by defining the hidden info class and applying it to our info DOM when the control is constructed if the control does have an info page (we are looking at you remember me) (q). It's also very easy for us to deal with the show info method since we just need to remove the hidden class before we do anything else. The slightly difficult part is when we want to hide the info we will modify our code to remove the class when the hide animation is complete. Now from the standpoint of the control group this doesn't really add any complication. Before we get to the complication we just need to define the hidden info class (r). Now we turn to our component's typescript class. The complication that I was mentioning stems from the fact that when we are showing an info page we are hiding the previous page and in so doing we are setting the onfinish function which will just over write what we added in the control group. To deal with this we will simple cache any onfinish function that already exists, set our own, and then call the cached function when the hide animation is complete (s).
control-group.ts (3)
...
export class ControlGroup {
...
private get hiddenInfoClass() {
return "hidden-info";
}
...
constructor(config: IControlGroupConfig) {
...
if (typeof this._infoId !== "undefined") {
this.infoDOM.classList.add(this.hiddenInfoClass);
}
}
...
public hideInfo = () => {
const animation = this.infoDOM.animate(this._hideInfoEffect, this._infoTiming);
animation.onfinish = () => {
this.infoDOM.classList.add(this.hiddenInfoClass);
};
return animation;
}
...
public showInfo = () => {
this.infoDOM.classList.remove(this.hiddenInfoClass);
...
}
...
}
authenticator.component.scss (3)
.authenticator {
.hidden-info {
display: none;
}
}
authenticator.component.ts (3)
...
export class AuthenticatorComponent ... {
...
private showInfo = (key: string) => {
...
let onfinish: AnimationEventListener;
switch(...) {
case true:
...
onfinish = animation.onfinish;
animation.onfinish = (evt: AnimationPlaybackEvent) => {
if (loDashIsNil(onfinish) === false) {
onfinish(evt);
}
...
};
return;
case false:
if (...) {
onfinish = this._blurHideAnimation.onfinish;
this._blurHideAnimation.onfinish = (evt: AnimationPlaybackEvent) => {
if (loDashIsNil(onfinish) === false) {
onfinish(evt);
}
...
};
} else {
...
}
return;
}
...
}
...
}
With these changes made the ui is performing almost as expected. It now only shows the scrollbars when a particular control is focused and its info page is tall enough to warrant it. I say that it is almost performing as expected because the way we have set it up now the default info page never has the hidden info class applied to it since it does not have an associated control group. If you guessed that we would be fixing this in the next section you would be absolutely correct.
You are not special default info page
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-group.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
Our solution to this current difficulty is the same as the one that we used when we wanted to animate the default info page. Instead of defining the hidden info class within the control group we will pass it in when they are constructed (v). This of course means we need to pass it into our control pair collection (w). And again since this is something needed for every control group we will pass it in to the constructor of our control pair collection and the collection will pass it into the control groups. Lastly we just need to define it within the authenticator component, pass it in when constructing the control pair collection, and add/remove it when hidding/showing the default info page (x).
control-group.ts (4)
...
export interface IControlGroupConfig {
...
hiddenInfoClass: string;
...
}
export class ControlPair {
...
private readonly _hiddenInfoClass: string = null;
...
constructor(config: IControlGroupConfig) {
...
this._hiddenInfoClass = config.hiddenInfoClass;
...
if (typeof this._infoId !== "undefined") {
this.infoDOM.classList.add(this._hiddenInfoClass);
}
}
...
public hideInfo = () => {
...
animation.onfinish = () => {
this.infoDOM.classList.add(this._hiddenInfoClass);
};
...
}
...
public showInfo = () => {
this.infoDOM.classList.remove(this._hiddenInfoClass);
...
}
...
}
control-pair-collection.ts (3)
...
export interface IControlPairCollectionConfig {
...
hiddenInfoClass: string;
...
}
...
export class ControlPairCollection {
...
private readonly _hiddenInfoClass: string = null;
...
constructor(config: IControlPairCollectionConfig) {
...
this._hiddenInfoClass = config.hiddenInfoClass;
...
}
public create = (left: ICreatePairConfig, right?: ICreatePairConfig) => {
const createConfig = (c: ICreatePairConfig) => {
return {
...
hiddenInfoClass: this._hiddenInfoClass,
...
} as IControlGroupConfig;
};
...
}
}
authenticator.component.ts (4)
...
export class AuthenticatorComponent ... {
...
private get hiddenInfoClass() {
return "hidden-info";
}
...
constructor(...) {
...
this._controlPairCollection = new ControlPairCollection({
...
hiddenInfoClass: this.hiddenInfoClass,
...
});
...
}
...
private hideInfo = (...) => {
if (key === null) {
...
const animation = this._defaultInfoDOM.animate(this._hideInfoEffect, this._infoTiming);
animation.onfinish = () => {
this._defaultInfoDOM.classList.add(this.hiddenInfoClass);
};
return animation;
}
...
}
...
private showInfo = (key: string) => {
if (key === null) {
...
this._defaultInfoDOM.classList.remove(this.hiddenInfoClass);
...
}
...
}
}