#15 We Need Access to the DOM
Thursday, January 11, 2018
In this article we will create three additional class that we will use when we want to interact with the DOM elements that make up our form control groups. We will use this class to enable us to do everything from focus an input, perform animations and respond to keyboard navigation.
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.
Diagram
We will start by defining the objects that we will be representing by three additional classes that we will be creating shown in (a). To start we have the control group, represented by the orange border, which is the base component. There is a control group for the username, password, confirm password, email, confirm email, and remember me controls. Next there is the control pair, represented by the blue border, which is a pair of control groups. Each control group is a component of a pair where for the username and remember me controls the other control group is undefined. Finally we have the control pair collection, represented by the purple border, which is the collection of all control pairs.
Control Groups Need An Id
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.pug
- authenticator.component.ts
-
authenticator
-
components
-
Source
In order to do things like animate our control groups and focus their inputs we will need references to the actual DOM elements that
make up the groups. To facilitate this we will add unique id's to each of the control groups. We start by adding a binding to the
attr.id
property shown in (b) in both the
formControlGroup
as well as on the remember me group itself. With the binding complete in the
template we need to actually define the properties in our typescript file for them to bind to (c).
authenticator.component.pug (1)
mixin formControlGroup(...)
div.form-control-group([attr.id]=`${key}GroupId`)
...
div.authenticator
div.inputs-container#inputs-container
h3 ...
form(...)
...
div.form-control-group.control-group-remember-me([attr.id]=`controlRememberMeGroupId`)
authenticator.component.ts (1)
...
export class AuthenticatorComponent implements OnInit {
...
public get controlEmailGroupId() { return "control-group-email"; }
public get controlEmailConfirmGroupId() { return "control-group-email-confirm"; }
public get controlPasswordGroupId() { return "control-group-password"; }
public get controlPasswordConfirmGroupId() { return "control-group-password-confirm"; }
public get controlRememberMeGroupId() { return "control-group-remember-me"; }
public get controlUsernameGroupId() { return "control-group-username"; }
...
}
Once those changes are completed and we save the files if we check the elements in the browser developer tools we should see that we now of an id applied to each of our control groups (d).
Every Problem Can Be Solved With More Classes
-
WebUi
-
Source
-
components
-
authenticator
- control-group.ts
- control-pair.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
Now it's time to create those new classes that we have been talking about. We start by first defining the the control group
(e). This class will be responsible for any interactions with the DOM so we will provide
an instance of the DOMReaderService
as well as the groups id of course. Next we
have the control pair which as we mentioned previously allows for the right control being optional (f).
And of course lastly we have the collection (g) that holds all of our control
pairs. When we create the control pair collection we will pass in all of the objects that are common to the creation of
a control group.
control-group.ts (1)
import { DOMReaderService } from "../../services/dom-reader.service";
export interface IControlGroupConfig {
domReader: DOMReaderService;
id: string;
}
export class ControlGroup {
private readonly _domReader: DOMReaderService = null;
private readonly _id: string = null;
constructor(config: IControlGroupConfig) {
this._domReader = config.domReader;
this._id = config.id;
console.log(`creating ${this._id}`);
}
}
control-pair.ts (1)
import { IControlGroupConfig, ControlGroup } from "./control-group";
export interface IControlPairConfig {
left: IControlGroupConfig;
right?: IControlGroupConfig;
}
export class ControlPair {
private readonly _left: ControlGroup = null;
private readonly _right: ControlGroup = null;
constructor(config: IControlPairConfig) {
this._left = new ControlGroup(config.left);
if (typeof config.right !== "undefined") {
this._right = new ControlGroup(config.right);
}
}
}
control-pair-collection.ts (1)
import { DOMReaderService } from "../../services/dom-reader.service";
import { ControlPair } from "./control-pair";
import { IControlGroupConfig } from "./control-group";
export interface ICreatePairConfig {
id: string;
}
export interface IControlPairCollectionConfig {
domReader: DOMReaderService;
}
export class ControlPairCollection {
private readonly _domReader: DOMReaderService = null;
private readonly _pairs: { [key: string]: ControlPair } = {};
constructor(config: IControlPairCollectionConfig) {
this._domReader = config.domReader;
}
public create = (left: ICreatePairConfig, right?: ICreatePairConfig) => {
const leftConfig = {
domReader: this._domReader,
id: left.id
} as IControlGroupConfig;
if (typeof right === "undefined") {
this._pairs[left.id] = new ControlPair({
left: leftConfig
});
return;
}
const pair = new ControlPair({
left: leftConfig,
right: {
domReader: this._domReader,
id: right.id
}
});
this._pairs[left.id] = pair;
this._pairs[right.id] = pair;
}
}
After the view is intialized
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
Now that we have the control group classes defined we need to use them in our component's typescript file
(h) so that we
can reference each control group. While we are doing this we must keep in mind that a control group will be accessing the DOM at some point when
we create them. This is because depending on when that accessing takes place angular may not have actually created the
DOM for our component. We can take one of two approaches: (a) be very careful every time that we make a change to these files that
we are not accessing the DOM to early or (b) only create a control group after angular has initialized the DOM for our
component. We will be opting for option (b). To do this we will make use of another interface within the angular core
package called AfterViewInit
. After we have indicated that our class implements
this interface we need to define the ngAfterViewInit
method. We will be using this
method to create our control groups.
authenticator.component.ts (2)
import { AfterViewInit, ... } from "@angular/core";
...
import { ControlPairCollection } from "./control-pair-collection";
...
export class AuthenticatorComponent implements AfterViewInit, ... {
private readonly _controlPairCollection: ControlPairCollection = null;
...
constructor(...) {
...
this._controlPairCollection = new ControlPairCollection({
domReader: this._domReader
});
}
public ngAfterViewInit(): void {
this._controlPairCollection.create(
{
id: this.controlEmailGroupId
},
{
id: this.controlEmailConfirmGroupId
});
this._controlPairCollection.create(
{
id: this.controlPasswordGroupId
},
{
id: this.controlPasswordConfirmGroupId
});
this._controlPairCollection.create({
id: this.controlRememberMeGroupId
});
this._controlPairCollection.create({
id: this.controlUsernameGroupId
});
}
...
}
When our code is run now we should see (i) indicating that we are creating all of our control groups.
Control group DOM
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- control-group.ts
- control-pair-collection.ts
-
authenticator
-
components
-
Source
Now that we are creating all of the control groups we need to give them access to their respective DOM elements. We will
do this by giving them a reference to the ElementRef
from angular which we have
seen previously. Just to remind us this is the object that will give us a reference to the actual DOM of our
component. Once we have this reference we will use it to find the DOM element for each control group using its id and
output it in the console (j). Next we need to modify the control pair collection
so that it will provide the reference that we need (k). And the last thing
we need to do, of course, is provide the reference to the collection (l).
control-group.ts (2)
import { ElementRef } from "@angular/core";
...
export interface IControlGroupConfig {
...
elementRef: ElementRef;
...
}
export class ControlGroup {
...
private readonly _elementRef: ElementRef = null;
...
private _groupDOM: HTMLElement = null;
private get groupDOM() {
if (this._groupDOM !== null) {
return this._groupDOM;
}
this._groupDOM = this._domReader.findChildById(this._elementRef.nativeElement, this._id);
return this._groupDOM;
}
constructor(config: IControlGroupConfig) {
...
this._elementRef = config.elementRef;
...
console.log(this.groupDOM);
}
}
control-pair-collection.ts (2)
import { ElementRef } from "@angular/core";
...
export interface IControlPairCollectionConfig {
...
elementRef: ElementRef;
}
export class ControlPairCollection {
...
private readonly _elementRef: ElementRef = null;
...
constructor(...) {
...
this._elementRef = config.elementRef;
}
public create = (...) => {
const leftConfig = {
...
elementRef: this._elementRef,
...
} as IControlGroupConfig;
...
this._pairs.push(new InputPair({
...
right: {
...,
elementRef: this._elementRef,
...
}
}));
}
}
authenticator.component.ts (3)
...
export class AuthenticatorComponent ... {
constructor(...) {
...
this._inputPairCollection = new ControlPairCollection({
...,
elementRef: this._elementRef
});
}
}
Once that is done we should see output similar to (m) in the browser console showing that we do have access to the DOM of each of our control groups.