Advertisement

#6 What is a Form Group?

In this article we will create the ability for our component to know whether or not the user is attempting to login or register and create an angular form group object to be the backing store for our inputs form. Along the way we will create a form controller class that will help us to access the controls contained within the form group.

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

It wouldn't feel natural if I didn't forget something

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

In the last article we created the markup and styling for the info/right side of our component and of course I did forgot one small part. The last thing that we did in the authenticator.component.scss file was to define the colors of our validator icons based on which font awesome class was applied. The last one that we set was for .fa-times which is the red . What I forgot was that we need to adjust the size of the font for this icon slightly as shown in (a).

authenticator.component.scss

&.fa-times {
    color: $invalid-color;
    font-size: 1.3em; /*I forgot to add this in the last article.*/
}
(a) Adjustment to the font size of the icon that I forgot to add in the last article.

Are we logging in or registering?

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

The last step of the previous article was to inject a uri parser service into the constructor of our component and we are now going to use this service to determine whether the user is logging in or registering. We start by adding a couple of field and property getters as shown in (b). As a matter of style if I am going to set the value of a field at some later point, say in the constructor, I set the value of the field to null. I am also including the public getters so that we can tell if we are logging in or registering in our template. Next, in the constructor, we will split the path property of our parsed url on the forward slash '/'. Since this property will be either '/account/login' or '/account/register' we will receive an array of length three where the third element will be the action with a value of either 'login' or 'register'. It's just a simple matter then to use the action to determine whether the user is attempting to login or register.

authenticator.component.ts (login or register - fields and properties)

private readonly _isLogin: boolean = null;
private readonly _isRegister: boolean = null;

public get isLogin() { return this._isLogin; }
public get isRegister() { return this._isRegister; }
(b) The fields a properties that we will set to indicate whether the user is logging in or registering.

authenticator.component.ts (login or register - constructor)

const parsedUrl = this._uriParser.parseUri(window.location.href);
const action = parsedUrl.path.split("/")[2].toLowerCase();
this._isLogin = action === "login";
this._isRegister = action === "register";
(c) Setting our login and register fields to the appropriate value depending on the action being taken.

Form Group

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

To process the information that the user enters we need access to the values in our inputs. To facilitate this access we first need to create what angular calls a form group. We do this by returning to our pug file and adding a property to our form tag as shown in (d). The square brackets are the way that we tell angular that we are setting a property on the dom element, in this instance the form, which of course does not natively have a property called 'formGroup'.

authenticator.component.pug (form group)

- var formGroup = "authForm";
...
div.authenticator
    div.inputs-container
        h3 Login
        form(novalidate, [formGroup]=formGroup)
...
(d) Adding the form group property to our form tag.

If we save our pug file and refresh the browser we will in fact see an error telling us that 'formGroup' is not a known property of 'form' (e).

Error telling us that 'formGroup' is not a known property of the form element.
(e) Error telling us that 'formGroup' is not a known property of the form element.
  • WebUi
    • Source
      • app
        • account-authenticator.site.ts

To eliminate this error we need to return to our module definition, located in the account-authenticator.site.ts file, and include another angular module. The 'ReactiveFormsModule' is located imported from the '@angular/forms' package. Once we have imported it within the file we need to include it in our module imports (f). Angular gives us two different ways of defining forms, which since we are importing the reactive forms module, you may be able to guess I prefer the reactive method but you may prefer the template driven approach. You can read more about both in the angular forms documentation.

account-authenticator.site.ts (form group)

import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";

import { AuthenticatorComponent } from "../components/authenticator/authenticator.component";

@NgModule({
    imports: [
        BrowserModule,
        FormsModule,
        ReactiveFormsModule
    ],
    declarations: [AuthenticatorComponent],
    bootstrap: [AuthenticatorComponent]
})
export class AuthenticatorModule { }

const platform = platformBrowserDynamic();
platform.bootstrapModule(AuthenticatorModule);
(f) Importing the 'ReactiveFormsModule' into our module definition.

If we save our file and refresh the browser we will see that we have fixed the previous error but now we have a new one. This error is essentially telling us that we need to do the setup of our form controls in our component's typescript file which we will do shortly.

Error telling us that we need to setup our form controls in the components typescript file.
(g) Error telling us that we need to setup our form controls in the components typescript file.
Advertisement

Form Controller

  • WebUi
    • Source
      • app
        • vendor.ts
      • forms
        • form.controller.ts

Before we turn to defining the form group we need to create a small helper class (h) that I like to use when I am dealing with an angular form. The main purpose of this class can be found in the getControl method. To access a control within a form group we use a string indexer on the controls object and the result if the control does not exist is 'undefined'. Typically when we are attempting to access a control it's to get its value and if it's undefined we will get the 'cannot access property of undefined' error message and then we need to sort out what is going on. I prefer an explicit error when I attempt to access a control that does not exist.

The last thing we need to handle with our form controller is the import of the lodash function forOwn which we can see being used in the updateAllValuesAndValidities method. If we do nothing our bundle will still compile and everything will work correctly the only drawback is that the forOwn function will be included in this bundle and not the vendor bundle. This would not be a problem if this is the only time that we will be using this function but most likely this is not the case. To remedy this we will return to our vendor.ts file and add the import statement there as well. This will mean that we will import the function only once and webpack will take care of handing out this single import to every file that requests it.

form.controller.ts

import loDashForOwn = require("lodash/forOwn");

import { FormBuilder, FormGroup, FormControl, ValidatorFn } from "@angular/forms";

export class FormController {
    public form: FormGroup = null;

    constructor(private readonly _fb: FormBuilder) { }

    public create = (config: { [key: string]: [string|boolean, ValidatorFn] }) => {
        this.form = this._fb.group(config);
    }

    public getControl = (name: string): FormControl => {
        if (typeof this.form === "undefined" || this.form === null) {
            throw new Error("Attempted to access a control before calling create.");
        }
        const control = this.form.controls[name];
        if (control) {
            return control as FormControl;
        }
        throw new Error(`The '${name}' control does not exist on the form.`);
    }

    public updateAllValuesAndValidities = () => {
        loDashForOwn(this.form.controls, c => {
            c.updateValueAndValidity();
        });
    }
}
(h) Small helper class that we will use to help us interact with a form group.

vendor.ts

...
import "lodash/forOwn";
...
(i)
  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts

Next we turn to modifying our components typescript file. We start by adding import statements for OnInit, FormBuilder, ValidatorFn, and FormController. Once we have imported everything we need to add the FormBuilder class to our providers array and add the implements statement to our class (j). Once we have imported everything we turn to adding the fields and properties (k) that we will need when creating our form controls. This includes the getter for the form group itself as well as the getters for the string keys of the controls and shortcuts for accessing the controls via the form controller. Lastly we create our form controller in the constructor and implement a bare bones ngOnInit method to create the controls for our form.

authenticator.component.ts (imports and metadata)

import { Component, OnInit } from "@angular/core";
import { FormBuilder, ValidatorFn } from "@angular/forms";
import { FormController } from "../../forms/form.controller";

@Component({
    selector: "authenticator",
    template: require("./authenticator.component.pug"),
    styles: [require("./authenticator.component.scss")],
    providers: [FormBuilder, UriParserService]
})
export class AuthenticatorComponent implements OnInit {
    ...
}
(j) The import statements as well as the addition of the FormBuilder to our providers array and the addition of the OnInit implementing statement to our class.

authenticator.component.ts (fields and properties)

private readonly _formController: FormController = null;

private get controlEmailKey() { return "controlEmail"; }
private get controlEmailConfirmKey() { return "controlEmailConfirm"; }
private get controlPasswordKey() { return "controlPassword"; }
private get controlPasswordConfirmKey() { return "controlPasswordConfirm"; }
private get controlRememberMeKey() { return "controlRememberMe"; }
private get controlUsernameKey() { return "controlUsername"; }

private get controlEmail() { return this._formController.getControl(this.controlEmailKey); }
private get controlEmailConfirm() { return this._formController.getControl(this.controlEmailConfirmKey); }
private get controlPassword() { return this._formController.getControl(this.controlPasswordKey); }
private get controlPasswordConfirm() { return this._formController.getControl(this.controlPasswordConfirmKey); }
private get controlRememberMe() { return this._formController.getControl(this.controlRememberMeKey); }
private get controlUsername() { return this._formController.getControl(this.controlUsernameKey); }

public get authForm() { return this._formController.form; }
(k) The fields and properties that we will need in order to create and then access our form controls.

authenticator.component.ts (constructor and methods)

constructor(private _uriParser: UriParserService, fb: FormBuilder) {
    ...

    this._formController = new FormController(fb);
}

public ngOnInit(): void {
    const controls: { [key: string]: [string|boolean, ValidatorFn] } = {};
    controls[this.controlEmailKey] = ["", null];
    controls[this.controlEmailConfirmKey] = ["", null];
    controls[this.controlPasswordKey] = ["", null];
    controls[this.controlPasswordConfirmKey] = ["", null];
    controls[this.controlRememberMeKey] = [false, null];
    controls[this.controlUsernameKey] = ["", null];
    this._formController.create(controls);
}
(l) Instantiating our form controller and using it to create our form controls.
  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.pug

Finally we will update our template so that we correctly match up each input with its corresponding form control in the typescript file. We start by defining the keys for our controls in (m) which are the same values that we used previously in our typescript file. In (m) we also need to modify our formControlGroup mixin to allow us to define the type of input and also add the name and formControlName properties. Now that we have modified our mixin we need to modify the calls to it (n). Lastly we will also modify the input used in our remember me control.

authenticator.component.pug (mixin formControlGroup)

- var controlEmailKey = "controlEmail";
- var controlEmailConfirmKey = "controlEmailConfirm";
- var controlPasswordKey = "controlPassword";
- var controlPasswordConfirmKey = "controlPasswordConfirm";
- var controlRememberMeKey = "controlRememberMe";
- var controlUsernameKey = "controlUsername";

mixin formControlGroup(label, inputType, key)
    div.form-control-group
        label #{label}
        div.input-group.invalid
            input(type=inputType, name=key, formControlName=key)
            button
                i.fa.fa-times
(m) Updating our form control group mixin to account for the input type, name, and form control name.

authenticator.component.pug (invoke formControlGroup))

+formControlGroup("Username", "text", controlUsernameKey)            

div.forgot
    a Forgot your username?

div.dual-input-group
    +formControlGroup("Password", "text", controlPasswordKey)
    +formControlGroup("Confirm Password", "text", controlPasswordConfirmKey)            

div.forgot
    a Forgot your password?

div.dual-input-group
    +formControlGroup("Email", "email", controlEmailKey)
    +formControlGroup("Confirm Email", "email", controlEmailConfirmKey)

div.form-control-group.control-group-remember-me
    input#remember-me(type="checkbox", name=controlRememberMeKey, formControlName=controlRememberMeKey)
(n) Updating the invocation of the form control group mixin.

In the next article we will start, if not finish, implementing the validation logic for our form.

Exciton Interactive LLC
Advertisement