A type safe way of creating Angular ReactiveForms and FormGroups

If, like myself, you prefer as many of your coding mistakes to be identified at compile time as possible then you might like the following example.

The FormBuilder in Angular expects us to identify our FormControls with a string name. If ever the API of your server changes then of course the names of those controls will also need to change. So, rather than using magic strings when building a FormGroup I wrote this small utility class for building them against a strongly typed class by using lambda expressions.

This routine uses a routine I blogged about recently ( Get lambda expression as a string)

Take the following interface as an example of something we wish to edit in a ReactiveForm.

interface Address {
    line1: string;
    line2: string;
    city: string;
}

The code to build the form would look like this
this.form = this.expressionFormBuilder
  .createFormGroup<Address>()
  .addFormControl(x => x.line1)
  .addFormControl(x => x.line2)
  .addFormControl(x => x.city)
  .build();

If you had a nested object like this
interface Person {
    name: string;
    address: Address;
}
interface Address {
    line1: string;
    line2: string;
    city: string;
}

Then you can also build your Reactive Form's sub-group in the same manner....
this.form = this.expressionFormBuilder
  .createFormGroup<Person>()
  .addFormControl(x => x.name)
  .addFormGroup(x => x.address, this.expressionFormBuilder
    .createFormGroup<Address>()
    .addFormControl(address => address.line1)
    .addFormControl(address => address.line2)
    .addFormControl(address => address.city)
    .build())
  .build();
There is some room for improvement, such as making the addFormGroup infer the type from the expression, but I'll leave that as an exercise for you to do, because I don't need it yet :)

Here is the code for the ExpressionFormBuild class. You can easily inject it into your component's constructor to get an instance, which you would then use in ngOnInit or in the component's constructor.

import { AsyncValidatorFn, FormArray, FormBuilder, FormGroup, ValidatorFn } from '@angular/forms';
import { Expression } from './expression';
import { Injectable } from '@angular/core';

/**
 * Builds reactive forms based on lambda expressions instead of string based names.
 */
@Injectable()
export class ExpressionFormBuilder {

  constructor(private formBuilder: FormBuilder) {}

  public createFormGroup<T>(): ExpressionFormBuilderState<T> {
    return new ExpressionFormBuilderState<T>(this.formBuilder);
  }
}

class ExpressionFormBuilderState<T> {
  private config = {};

  constructor(private formBuilder: FormBuilder) {}

  /**
   * Builds a FormGroup from the current state
   * @returns {FormGroup}
   */
  public build() {
    return this.formBuilder.group(this.config);
  }
  /**
   * Adds a form control for the specified name
   * @param {(t: T) => any} name A lambda expression identifying the member, e.g. x => x.firstName
   * @param {string} defaultValue The default value to give to the form control
   * @param {ValidatorFn} validator Optional validators to add to the FormControl
   * @param {AsyncValidatorFn} asyncValidator Optional async validators to add to the FormControl
   */
  public addFormControl(
    name: (t: T) => any,
    defaultValue?: string|null,
    validator?: ValidatorFn|null,
    asyncValidator?: AsyncValidatorFn|null
  ): ExpressionFormBuilderState<T> {
    const controlName = Expression.path<T>(name);
    this.config[controlName] = [defaultValue, validator, asyncValidator];
    return this;
  }

  /**
   * Adds a sub group
   * @param {string} name
   * @param group
   * @returns {ExpressionFormBuilderState}
   */
  public addGroup(name: (t: T) => any, group: any): ExpressionFormBuilderState<T> {
    const groupName = Expression.path<T>(name);
    this.config[groupName] = group;
    return this;
  }
}

Comments

Popular posts from this blog

Connascence

Convert absolute path to relative path

Printing bitmaps using CPCL