Advertisement

#15 We Need Access to the DOM

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.

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.

Diagram of the class make up of our controls. The orange group is the control
            group, which is the base component. Next the blue group defines a control pair which is of course a pair of control
            groups. Each control is a member of the control pair. Finally the purple group is the control pair collection which
            is the collection of all control pairs.
(a) Diagram of the class make up of our controls. The orange group is the control group, which is the base component. Next the blue group defines a control pair which is of course a pair of control groups. Each control is a member of the control pair. Finally the purple group is the control pair collection which is the collection of all control pairs.

Control Groups Need An Id

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

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`)
(b) Binding properties of our typescript file to the id property of all of the control groups.

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"; }
    ...
}
(c) Defining the properties that will be bound to the control group ids.

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).

Verifying that the changes we have just made do in fact result in an
            id being applied to each of our control groups.
(d) Verifying that the changes we have just made do in fact result in an id being applied to each of our control groups.

Every Problem Can Be Solved With More Classes

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

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}`);
    }
}
(e) Defining our base component the control group. This class will be responsible for the interaction with the DOM so we start with providing its id and an instance of the DOMReaderService

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);
        }
    }
}
(f) Defining the control pair class which consists of a pair of control groups. We also allow for the fact that not every control group has a pair group so the right control group is optional.

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;
    }
}
(g) Defining the control pair collection which is responsible for maintaining a reference to each control pair as well as passing in arguments that are common to the creation of each control group.
Advertisement

After the view is intialized

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

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
        });
    }
    ...
}
(h) Creating an instance of the control pair collection in the constructor of our component but not creating any control groups until the ngAfterViewInit method where it is now safe to try accessing the DOM.

When our code is run now we should see (i) indicating that we are creating all of our control groups.

Console output showing that we are creating all of the control groups that we are
            expecting to.
(i) Console output showing that we are creating all of the control groups that we are expecting to.

Control group DOM

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

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);
    }
}
(j) Our control will use the ElementRef from angular to have lazy access to its DOM.

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,
                ...
            }
        }));
    }
}
(k) We will pass in a reference to the ElementRef in the constructor of our collection and then use that every time we create a control pair.

authenticator.component.ts (3)

...
export class AuthenticatorComponent ... {
    constructor(...) {
        ...
        this._inputPairCollection = new ControlPairCollection({
            ...,
            elementRef: this._elementRef
        });
    }
}
(l) We just need to pass in the ElementRef to the constructor of our control pair collection.

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.

Console window output showing the DOM element for each of our control groups.
(m) Console window output showing the DOM element for each of our control groups.
Exciton Interactive LLC
Advertisement