Building scalable robusts and type safe forms with Angular

Maxime - Jun 16 '19 - - Dev Community

Hi there šŸ‘‹!

Today I'd like to share some of my experience in building (what I think are) large and complex forms with Angular. One thing to keep in mind though; if you don't feel concerned by the "scalable" or "large forms" parts, be aware that all of the following can be still be applied to super tiny forms and you'll still get a lot of benefits!

Disclaimer: I have not been the only one spending hours to think about a better way of handling forms. Zak Henry (@zak) and I both came up to the solution I'll be introducing today and I'd like to thank him for all the time he has spent thinking on the design, coding, and making code reviews too šŸ‘ šŸ‘ šŸ‘.

Table of contents

Context

Before we dig into the main topic, let me give you a little bit of context to explain why I am building large forms at work and why I really needed to find a solution doing a better job at this.

I'm currently working at CloudNC, a startup in London where we aim to greatly simplify, speed up and improve the process of machining a part using a CNC milling machine. Within our application, we need to model the environment (machines, tools, etc) as input to algorithms that generate the machine movements. This environment has a huge number of parameters, so we have quite a lot of forms.


3D visualiser to simulate how the machine is going to cut the part.

A good example with one of our forms is when we want to annotate a specific hole on a part. We can choose amongst different types of annotations and select a specific behavior:


Example of 2 forms to annotate a hole.

With this example you can see we have two separate forms, each selected by one dropdown (this forms a polymorphic model - explained later!). Each of those forms uses the same "Spotting Strategy" sub form. This isn't even our most complex form, so you can begin to understand the motivation to come up with a generic solution for breaking up forms into logical components that can be easily composed.

Introduction to the demo and understand what we want to build as an example

In order to showcase when it could be difficult to properly build a good form, we've built an app. The main idea being: "Galactic sales" where someone can decide to sell either a Droid (Assassin, Astromech, Medical, Protocol) or a Vehicle (Spaceship, Speeder).

On the left, you can see a simple list showing the items for sale.

On the right, there's a form showing the item that has been clicked (so it can be edited, think of it as an admin view). It's also possible to create a new entry by clicking the "Create new" button.

If you want to play with the live version of the demo, you can have a go here: https://cloudnc.github.io/ngx-sub-form

If you want to take a look into the source code of the demo, you can go here: https://github.com/cloudnc/ngx-sub-form/tree/master/src/app

Now let's take a closer look at the models (interfaces, enums, etc) to build a simplified version of it. Enough to understand all the concepts but avoid repetition.

A Listing is an item for sale:

// can either be a vehicle or a droid
export enum ListingType {
  VEHICLE = 'Vehicle',
  DROID = 'Droid',
}

export interface BaseListing {
  id: string;
  title: string;
  imageUrl: string;
  price: number;
}

export interface VehicleListing extends BaseListing {
  listingType: ListingType.VEHICLE;
  product: OneVehicle;
}

export interface DroidListing extends BaseListing {
  listingType: ListingType.DROID;
  product: OneDroid;
}

export type OneListing = VehicleListing | DroidListing;
Enter fullscreen mode Exit fullscreen mode

Let's take a look at the Vehicle model for now:

export enum VehicleType {
  SPACESHIP = 'Spaceship',
  SPEEDER = 'Speeder',
}

export interface BaseVehicle {
  color: string;
  canFire: boolean;
  numberOfPeopleOnBoard: number;
}

export interface Spaceship extends BaseVehicle {
  vehicleType: VehicleType.SPACESHIP;
  numberOfWings: number;
}

export interface Speeder extends BaseVehicle {
  vehicleType: VehicleType.SPEEDER;
  maximumSpeed: number;
}

export type OneVehicle = Spaceship | Speeder;
Enter fullscreen mode Exit fullscreen mode

One important thing to note here, is that OneListing is a polymorphic type (and so is OneVehicle):

export type OneListing = VehicleListing | DroidListing;
Enter fullscreen mode Exit fullscreen mode

The object that will hold a value here can either be a VehicleListing or a DroidListing. Typescript could ensure type safety in that case only by looking at the properties, but to be safer and also later have an easy way of knowing the type of an object, we use a discriminant property: listingType.

Now let's move on to the next step and understand the challenge of building a form that can represent that data structure.

Reactive forms

They've been introduced in the early versions of Angular and completely changed the way to reason about forms. In a good way! Instead of managing all the logic of our forms from the templates, we can manage it from our Typescript/Components files. Plus we can enjoy type safety on all of our forms!

Well... Not exactly. There's a long standing issue on Github.

Reactive forms are not strongly typed #13721

[x] feature request
  • Angular version: 2

Reactive forms is meant to be used in complex forms but control's valueChanges are Observable<any>, which are totally against good practices for complex code.

There should be a way to create strongly typed form controls.

I invite you to show some support by putting a šŸ‘ on the post so that it eventually can be prioritised.

Even though we can't really benefit from type safety currently, we can still build it using a reactive form. Let's walk through the different solutions to do so.

Everything in one file šŸ”„

One solution would be to put everything in the same component. As you can imagine, this is far from ideal:

  • Huge file
  • Hard to work on the same file in parallel with multiple developers
  • Not splitting the logic in different groups to break down the problem into smaller pieces
  • Can't reuse sub parts of the form (to edit only a smaller subset for example)

Breaking down the form into sub components šŸ‘

So just like you would with any other component or function, you start to think about how to break it down into simpler and smaller sub components handling their own logic. From there, you'll probably find a few blog posts and Stack Overflow questions that recommend to pass the form group instance as an @Input() and then from a sub component dynamically add the required new form properties.

But something doesn't feel right, does it? If it wasn't a form, just a simple object, would you pass the Listing object to a Vehicle component? It'd then have access to the property listing.vehicle which is fine, but also the listing itself, which feels dangerous. Worst, it'd be able to mutate listing.vehicle (to add/remove properties). Following the one way data binding principle, you realize this would be wrong. I think this also applies to forms.

Breaking down the form into sub components, the right way! šŸŽ‰

Kara talked about ControlValueAccessor in this brilliant talk at Angular Connect in 2017: https://youtu.be/CD_t3m2WMM8

In case you've never heard of ControlValueAccessor, it'll let you create a component that can be used as a FormControl. Basically, you could do something like the following:

@Component({
  selector: 'my-custom-input',
  // ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyCustomInput),
      multi: true,
    },
  ],
})
export class MyCustomInput implements ControlValueAccessor {
  writeValue(obj: any): void {
    // every time the form control is
    // being updated from the parent
  }
  registerOnChange(fn: any): void {
    // when we want to let the parent
    // know that the value of the
    // form control should be updated
    // call `fn` callback
  }
  registerOnTouched(fn: any): void {
    // when we want to let the parent
    // know that the form control
    // has been touched
    // call `fn` callback
  }
  setDisabledState?(isDisabled: boolean): void {
    // when the parent updates the
    // state of the form control
  }
}
Enter fullscreen mode Exit fullscreen mode

Can you spot the main difference between passing the FormGroup instance and using ControlValueAccessor? It's now pretty much the same pattern that we use when we make a dumb component!

The following is not something available in Angular but just to make my point, you could think of it this way:

  @Input() set value(obj: any) {}
  @Input() set disabledState(isDisabled: boolean): void {}

  @Output() updated: EventEmitter<any> = new EventEmitter();
  @Output() touched: EventEmitter<any> = new EventEmitter();
Enter fullscreen mode Exit fullscreen mode

Note that:

  • The one way data flow is respected
  • The sub component doesn't have access to the whole form

If you take a look into into Angular Material source code, you'll notice that it is how a lot of components (acting as inputs) are built! For example, select, radio, slide-toggle, slider, ...

Understanding the power of ControlValueAccessor

While writing the previous example, I've created an empty class that implements ControlValueAccessor and then my IDE generated the required methods. But let's take a closer look at one of them:

writeValue(obj: any): void
Enter fullscreen mode Exit fullscreen mode

Have you noticed the name of the parameter? obj. That is interesting šŸ¤”.

You can pass any kind of object to writeValue! You're not limited to the primitive types like string or number. You can pass an object, an array, ... Everything we need to start building deeply nested forms.

So, let say that we want to build a form that will handle the following object:

export interface Spaceship {
  name: string;
  builtInYear: number;
  config: {
    maxSpeed: number;
    nbCanons: number;
  };
}
Enter fullscreen mode Exit fullscreen mode

We could:

  • Create a SpaceshipForm component
  • This component would handle only the primitive values (here name and builtInYear) and it'd request the config from another component
  • Create a sub component SpaceshipConfigForm which would itself handle only the primitive values (all of them in that case)

But let's take a look at the first level component (SpaceshipForm) to see why it'd be a small burden to do that on every sub component:

@Component({
  selector: 'my-custom-input',
  // ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MyCustomInput),
      multi: true,
    },
  ],
})
export class MyCustomInput implements ControlValueAccessor, OnInit, OnDestroy {
  private onChange: (value: any) => void;
  private onTouched: () => void;
  private onDestroy$: Subject<void> = new Subject();

  public internalFormGroup: FormGroup = new FormGroup({
    name: new FormControl(),
    builtInYear: new FormControl(),
    config: new FormControl(),
  });

  public ngOnInit(): void {
    this.internalFormGroup.valueChanges
      .pipe(
        tap(value => {
          this.onChange(value);
          this.onTouched();
        }),
        takeUntil(this.onDestroy$),
      )
      .subscribe();
  }

  public ngOnDestroy(): void {
    this.onDestroy$.next();
    this.onDestroy$.complete();
  }

  writeValue(obj: any): void {
    // every time the form control is
    // being updated from the parent

    this.internalFormGroup.setValue(obj, { emitEvent: false });

    this.internalFormGroup.markAsPristine();
    this.internalFormGroup.markAsUntouched();
  }
  registerOnChange(fn: any): void {
    // when we want to let the parent
    // know that the value of the
    // form control should be updated

    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    // when we want to let the parent
    // know that the form control
    // has been touched

    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    // when the parent updates the
    // state of the form control

    if (isDisabled) {
      this.internalFormGroup.disable();
    } else {
      this.internalFormGroup.enable();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The logic itself is not really complicated, but one thing sure is: It is verbose. That's only a minimal example and you could want to push things even further than that. But it's already ~75 lines. It seems to me that it is too much to just handle 2 inputs from that component šŸ¤·ā€ā™‚ļø.

Hopefully, you're about to find out why this blog post has been written (about time huh?) and how you can handle the above code in a better way.

Ngx-Sub-Form: Utility library to break down an Angular form into multiple sub components

As briefly explained in the context part of that article, we've been working at CloudNC on a library that will handle all the boilerplate for you and give you some nice helpers. It's called ngx-sub-form.

The library is published on both Github and NPM with an MIT license and is ready to be used. We've made a complete example/demo on the Github repository which is well tested too (both E2E and integration). We've also been rewriting a lot of our own forms with it and we believe that it is stable enough to be used by others now, so feel free to give it a try on your own projects!

What ngx-sub-form has to offer

  • Easily create sub forms or custom ControlValueAccessor with a minimal boilerplate
  • Type safety on both .ts and .html files for your components
  • Access all the values of the form, even the nested ones
  • Access all the errors of the form, even the nested ones (which is not natively supported on FormGroups, see https://github.com/angular/angular/issues/10530)
  • You should be able to mostly deal with synchronous values without having to deal with streams
  • Remap your original data to the shape you want for every form/sub form (and still keep it type safe)
  • Handle polymorphic data within your forms in an easy way

When should you use it?

Do you have a form?

Yes

Then use it.

My form is small and pretty simple, and I'm not sure it'll be worth it

You'll get type safety, less boilerplate and some helpers for free.

Unless you really have only one FormControl to handle one search input, you should just use it.

Enough words, show me some code!

Building the demo

We'll now refactor the previous component except that this time, we'll build the whole form (including the config!).

Here's the interface as a reminder:

Which I've broken down into 2 now, as we normally would/should!

export interface SpaceshipConfig {
  maxSpeed: number;
  nbCanons: number;
}

export interface Spaceship {
  name: string;
  builtInYear: number;
  config: SpaceshipConfig;
}
Enter fullscreen mode Exit fullscreen mode

Looking at the interface, we can easily guess what architecture we want here:

  • spaceship-container: Smart component that will inject a service to either retrieve a spaceship or save it when the form is valid and being sent
  • spaceship-form: Dumb component that is the top form, which will be in charge of handling the first values of the Spaceship
  • spaceship-config-form: Dumb component that is a sub form, only in charge of the config part

We'll start from the bottom component and then go up from there.
Here's the live demo of what I'm about to introduce: https://stackblitz.com/edit/ngx-sub-form-basics

You can play with and follow along on Stackblitz when you want to see the code in context.

spaceship-config-form.component.ts:

@Component({
  selector: 'app-spaceship-config-form',
  templateUrl: './spaceship-config-form.component.html',
  providers: subformComponentProviders(SpaceshipConfigFormComponent),
})
export class SpaceshipConfigFormComponent extends NgxSubFormComponent<SpaceshipConfig> {
  protected getFormControls(): Controls<SpaceshipConfig> {
    return {
      maxSpeed: new FormControl(),
      nbCanons: new FormControl(),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, this is a sub form component and the boilerplate is minimal.

First, let's look at the line

providers: subformComponentProviders(SpaceshipConfigFormComponent),
Enter fullscreen mode Exit fullscreen mode

When building a ControlValueAccessor, you need to at least provide NG_VALUE_ACCESSOR and also pass NG_VALIDATORS if you want to be able to deal with validation. It'd normally look like the following:

{
  // ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => component),
      multi: true,
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => component),
      multi: true,
    },
  ];
}
Enter fullscreen mode Exit fullscreen mode

All of that is reduced to one line with the utility function subformComponentProviders where you simply need to provide the current class. It also works with the AoT compiler.

Then, notice the line:

export class SpaceshipConfigFormComponent extends NgxSubFormComponent<SpaceshipConfig>
Enter fullscreen mode Exit fullscreen mode

By just doing that, you get access to a lot of

properties:

  • formGroup: The actual form group, useful to define the binding [formGroup]="formGroup" to the view
  • formControlNames: All the control names available in your form. Use it when defining a formControlName like so: <input [formControlName]="formControlNames.yourControl"> and it'll catch errors at build time (with AOT) if you change an interface!
  • formGroupControls: All the controls of your form, helpful to avoid doing formGroup.get(formControlNames.yourControl); instead just do formGroupControls.yourControl
  • formGroupValues: Access all the values of your form directly without doing formGroup.get(formControlNames.yourControl).value, instead just do formGroupValues.yourControl (and it'll be correctly typed!)
  • formGroupErrors: All the errors of the current form including the sub errors (if any). Just use formGroupErrors or formGroupErrors?.yourControl. Notice the question mark in formGroupErrors?.yourControl, it will return null if there's no error

methods:

  • onFormUpdate hook: Allows you to react whenever the form is being modified. Instead of subscribing to this.formGroup.valueChanges or this.formControls.someProp.valueChanges you will not have to deal with anything asynchronous nor have to worry about subscriptions and memory leaks. Just implement the method onFormUpdate(formUpdate: FormUpdate<FormInterface>): void and if you need to know which property changed do a check like the following: if (formUpdate.yourProperty) {}. Be aware that this method will be called only when there are either local changes to the form or changes coming from subforms. If the parent either calls setValue or patchValue, this method won't be called
  • getFormGroupControlOptions hook: Allows you to define control options for construction of the internal FormGroup. Use this to define FormGroup-level validators
  • handleEmissionRate hook: Allows you to define a custom emission rate (top level or any sub level)

Now you must think that it starts to be a lot and that hopefully we won't add to much after that. In that case, you'll be happy to know that this covers most of ngx-sub-form and the rest will now feel like deja-vu.

spaceship-config-form.component.html:

<fieldset [formGroup]="formGroup">
  <legend>Config</legend>

  Maximum speed
  <input type="number" [formControlName]="formControlNames.maxSpeed" />

  Number of canons
  <input type="number" [formControlName]="formControlNames.nbCanons" />
</fieldset>
Enter fullscreen mode Exit fullscreen mode

No further explanation needed for the html I think.

Now, let's move on to the top level form.

spaceship-form.component.ts:

@Component({
  selector: 'app-spaceship-form',
  templateUrl: './spaceship-form.component.html',
})
export class SpaceshipFormComponent extends NgxRootFormComponent<Spaceship> {
  @DataInput()
  @Input('spaceship')
  public dataInput: Spaceship | null | undefined;

  @Output('spaceshipUpdated')
  public dataOutput: EventEmitter<Spaceship> = new EventEmitter();

  protected getFormControls(): Controls<Spaceship> {
    return {
      name: new FormControl(),
      builtInYear: new FormControl(),
      config: new FormControl(),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

The purpose of that component is not to be bound to a FormControl like previous ones but instead:

  • To be able to update the form from an Input (dataInput, that you can of course rename)
  • To be able to share (emit) the new value once the form is valid and saved (through the Output dataOutput, that you can also rename)

Note: When using NgxRootFormComponent, dataInput does require you to use the @DataInput() decorator with it. The reason behind that is simple: ngx-sub-form has no way to know how you're going to rename your input and it does need to have a hook to be warned when that value changes. Instead of asking you to call a method from the ngOnChanges hook or to create a setter on the input and call that same method, we take care of all of that with the decorator.

Let's take a quick look at the view.

spaceship-form.component.html:

<form [formGroup]="formGroup">
  <fieldset>
    <legend>Spaceship</legend>
    Name
    <input type="text" [formControlName]="formControlNames.name" />

    Built in year
    <input type="number" [formControlName]="formControlNames.builtInYear" />

    <app-spaceship-config-form [formControlName]="formControlNames.config"></app-spaceship-config-form>

    <button (click)="manualSave()">Save</button>
  </fieldset>
</form>

<pre>{{ formGroup.value | json }}</pre>
Enter fullscreen mode Exit fullscreen mode

Nothing particularly new but note that we can display the whole form value (for debugging purpose here) by using formGroup.value and also that to save the form we simply call the manualSave method offered by ngx-sub-form when using NgxRootFormComponent.

Once we call manualSave, if the form is valid it'll emit the form's value through the output. This way the parent can simply retrieve a new Spaceship object and deal with it (put it into local storage, pass it to a service to make an HTTP call with it, etc). But the form is responsible only for the form itself, and the smart component (parent) is not even aware of the form. It is simply aware that a new value is available.

Speaking of which, let's take a look to the parent component.

spaceship-container.component.ts

// only to demo that passing a value as input will update the form
// but this info might come from a server for example when you want to
// edit an existing value
const getDefaultSpaceship = (): Spaceship => ({
  name: 'Galactico',
  builtInYear: 2500,
  config: {
    maxSpeed: 8000,
    nbCanons: 10,
  },
});

@Component({
  selector: 'app-spaceship-container',
  templateUrl: './spaceship-container.component.html',
})
export class SpaceshipContainerComponent {
  public spaceship$: Subject<Spaceship> = new Subject();

  public preFillForm(): void {
    this.spaceship$.next(getDefaultSpaceship());
  }

  public spaceshipUpdated(spaceship: Spaceship): void {
    // from here, you can pass that value to a
    // service to save/update it on a backend for example
    console.log(spaceship);
  }
}
Enter fullscreen mode Exit fullscreen mode

Interesting thing here - that smart component that would retrieve the data to populate the form and collect the new values of the form when it's being saved is only dealing with object of type Spaceship. Not a single FormGroup instance or FormControl is declared/used. We just delegate that responsibility to SpaceshipFormComponent. This is really convenient because if later in the app we want to display a Spaceship we could just reuse the form and disable it, so it'd be readonly. How so? NgxRootFormComponent and NgxAutomaticRootFormComponent both have an optional input property disabled. Simply bind a boolean to it and when the value is true, the form will be readonly. To be more specific, the whole form will be readonly, that includes all the sub form components too šŸ‘.

The view here is pretty simple, but for the sake of completeness:

spaceship-container.component.html

<button (click)="preFillForm()">Pre fill form (demo)</button>

<app-spaceship-form [spaceship]="spaceship$ | async" (spaceshipUpdated)="spaceshipUpdated($event)"></app-spaceship-form>
Enter fullscreen mode Exit fullscreen mode

Going further with remapping and/or polymorphism

So far, we've seen how to break down a form into smaller components.
The last remaining bit is now to discover how to handle forms with polymorphic data.

Let's start with a small reminder about polymorphism (Wikipedia):

In programming languages and type theory, polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

Example using the interfaces from the beginning of the post:

export type OneListing = VehicleListing | DroidListing;
Enter fullscreen mode Exit fullscreen mode

One object of type OneListing can either be a VehicleListing or a DroidListing.

Even if it might not be the kind of structure you'll be using all the time with your forms, it is a fairly common use case and ngx-sub-form offers a dedicated class to deal with that: NgxSubFormRemapComponent<ControlInterface, FormInterface>.

Note that NgxRootFormComponent and NgxAutomaticRootFormComponent are both using NgxSubFormRemapComponent as a base class so you can use the provided methods to remap within those too.

From here, the idea will be to create a new interface for that part of the form, which will have:

  • One entry to know what is the currently selected type
  • A different entry for every possible type

Example in our case:

export type OneListing = VehicleListing | DroidListing;

export enum OneListingType {
  VEHICLE = 'Vehicle',
  DROID = 'Droid',
}

export interface OneListingForm {
  listingType: OneListingType | null;
  vehicle: VehicleListing | null;
  droid: DroidListing | null;
}
Enter fullscreen mode Exit fullscreen mode

Then we can create our component like the following:

@Component({
  selector: 'app-one-listing-form',
  templateUrl: '...',
})
export class OneListingForm extends NgxRootFormComponent<OneListing, OneListingForm> {
  // note that we use `NgxRootFormComponent` as it extends `NgxSubFormRemapComponent`
  // and this is the top level form; that's why we will be using `NgxRootFormComponent`
  // ...
}
Enter fullscreen mode Exit fullscreen mode

At that point, Typescript will show an error and tell you that you need to correctly implement the class.

@Component({
  selector: 'app-one-listing-form',
  templateUrl: '...',
})
export class OneListingForm extends NgxSubFormRemapComponent<OneListing, OneListingForm> {
  @DataInput()
  @Input('listing')
  public dataInput: OneListing | null | undefined;

  @Output('listingUpdated')
  public dataOutput: EventEmitter<OneListing> = new EventEmitter();

  public OneListingType: typeof OneListingType = OneListingType;

  protected getFormControls(): Controls<OneListingForm> {
    return {
      listingType: new FormControl(null, { validators: [Validators.required] }),
      vehicle: new FormControl(null),
      droid: new FormControl(null),
    };
  }

  protected transformToFormGroup(obj: OneListing): OneListingForm {
    return {
      listingType: obj.listingType,
      vehicle: obj.listingType === OneListingType.VEHICLE ? obj : null,
      droid: obj.listingType === OneListingType.DROID ? obj : null,
    };
  }

  protected transformFromFormGroup(formValue: OneListingForm): OneListing | null {
    switch (formValue.listingType) {
      case OneListingType.VEHICLE:
        return formValue.vehicle;
      case OneListingType.DROID:
        return formValue.droid;
      case null:
        return null;
      default:
        throw new UnreachableCase(formValue.listingType);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

As you probably noticed, we're using 2 new methods to deal with polymorphic data:

  • transformToFormGroup: Passes as argument the value that is being written on that sub component (or via the dataInput if using a top level component) and remap that value to an internal one that splits the polymorphic object into separated entities
  • transformFromFormGroup: Passes the value of the form and expect as output to have an object matching the original interface

Within the transformToFormGroup method, it will be really straightforward if your different objects have a discriminator property. In our case, they do:

export interface VehicleListing extends BaseListing {
  listingType: ListingType.VEHICLE; // discriminator
  product: OneVehicle;
}

export interface DroidListing extends BaseListing {
  listingType: ListingType.DROID; // discriminator
  product: OneDroid;
}

export type OneListing = VehicleListing | DroidListing;
Enter fullscreen mode Exit fullscreen mode

The listingType property is common to VehicleListing and DroidListing but is set to a well defined value in both cases. This allows us to do a really simple check as follow:

protected transformToFormGroup(obj: OneListing): OneListingForm {
  return {
    listingType: obj.listingType,
    vehicle: obj.listingType === OneListingType.VEHICLE ? obj : null,
    droid: obj.listingType === OneListingType.DROID ? obj : null,
  };
}
Enter fullscreen mode Exit fullscreen mode

If you don't have a discriminator on your properties, you will need to somehow differentiate them based on other properties but I'd strongly advise you to add a discriminator if you have control of the interface as it will make things easier, not only for your forms.

The transformFromFormGroup method is also straightforward - based on the form value listingType we will just return the appropriate value from the form:

protected transformFromFormGroup(formValue: OneListingForm): OneListing | null {
  switch (formValue.listingType) {
    case OneListingType.VEHICLE:
      return formValue.vehicle;
    case OneListingType.DROID:
      return formValue.droid;
    case null:
      return null;
    default:
      throw new UnreachableCase(formValue.listingType);
  }
}
Enter fullscreen mode Exit fullscreen mode

The line throwing an error should never happen and is here mainly for type safety reasons as it'll force you to implement all the cases:

throw new UnreachableCase(formValue.listingType);
Enter fullscreen mode Exit fullscreen mode

If you don't have something similar in your own project, you could put the following in a shared folder

export class UnreachableCase {
  constructor(payload: never) {}
}
Enter fullscreen mode Exit fullscreen mode

If in future we add or remove one of the values to the OneListingType enum, Typescript will throw an error and make sure we don't forget any of the use cases.

Summary and take away

This article is now coming to an end and I hope you enjoyed this (new?) way of working with forms. However as we went pretty deep into details and it's almost a 20 minute read, I'd like to make a small summary of the important bits that you should remember:

  • Break down your forms, it'll make things easier to reason about and deal with
  • Prefer a ControlValueAccessor rather than passing your FormGroup or FormControl as inputs to a sub component
  • Put on your gloves, armor and helmet, stare at your terminal for a few seconds and run yarn install ngx-sub-form to avoid the boilerplate of creating a custom ControlValueAccessor (plus enjoy some nice helpers and type safety!)
  • If you're building a top level form component, make your choice between NgxRootFormComponent and NgxAutomaticRootFormComponent (manual save or instant save as soon as there's a change)
  • If you're building a sub form component, choose between NgxSubFormComponent and NgxSubFormRemapComponent depending whether you need to remap your data or not
  • Enjoy

Thanks for reading, this is my first blog post ever so please let me know how you found it so I can improve for the next ones! We might disagree on some point, or I might not have explained well enough. Either way I want to know, so please don't be shy and share what you didn't like, what I could have skipped, if it was too long, not detailed enough, etc.

If you have other ideas to improve ngx-sub-form, feel free to visit the Github project and either write an issue or make a pull request šŸ”„.

Useful links




Thanks again for reading!

Found a typo?

If you've found a typo, a sentence that could be improved or anything else that should be updated on this blog post, you can access it through a git repository and make a pull request. Instead of posting a comment, please go directly to https://github.com/maxime1992/my-dev.to and open a new pull request with your changes. If you're interested how I manage my dev.to posts through git and CI, read more here.

Follow me

Ā  Ā  Ā  Ā  Ā  Ā 
Dev Github Twitter Reddit Linkedin Stackoverflow

You may also enjoy reading

. . . . . . . . . . . . . . . . . .
Terabox Video Player