Skip to content

lppedd/ngx-sub-form-test

Repository files navigation

NgxSubForm

ngx-sub-form logo

Utility library for breaking down a form into multiple components.
Works well with polymorphic data structures.

ngx-sub-form is here to help you avoid passing your formGroup as inputs and tackle down the boilerplate of creating a custom ControlValueAccessor.

Install

Install the npm-package:

Usage

Before we get started with how to use the library and give some examples, a complete demo is available on this repo, within the src folder.
Demo is built around a concept of galactic sales. You can sell either Droids (Protocol, Medical, Astromech, Assassin) or Vehicles (Spaceship, Speeder). This will also be used for the following examples
.

First component level

Within the component where the form will be handled, we have to define the top level structure of the form, with each polymorphic type having it's own form control

public listingForm: FormGroup = new FormGroup({
  droidListing: new FormControl(null, Validators.required),
  vehicleListing: new FormControl(null, Validators.required),
  listingType: new FormControl(null, Validators.required)
});

and give access to our enum from the component:

public ListingType = ListingType;

Just as a sidenote here, here's the ListingType enum:

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

Then, within the .component.html we create a select tag to choose between the 2 types:

<select formControlName="listingType">
  <option *ngFor="let listingType of ListingType | keyvalue" [value]="listingType.value">
    {{ listingType.value }}
  </option>
</select>

Now we need to create, based on the listing type, either a DroidListingComponent or a VehicleListingComponent:

<form [formGroup]="listingForm">
  <div [ngSwitch]="listingForm.get(formControlNames.listingType).value">
    <app-droid-listing *ngSwitchCase="ListingType.DROID" formControlName="droidListing"></app-droid-listing>
    <app-vehicle-listing *ngSwitchCase="ListingType.VEHICLE" formControlName="vehicleListing"></app-vehicle-listing>
  </div>

  <button mat-raised-button (click)="upsertListing(listingForm.value)" [disabled]="listingForm.invalid">
    Upsert
  </button>
</form>

One thing to notice above:

  • formControlName="droidListing" our sub form component IS a custom ControlValueAccessor and let us bind our component to a formControlName as we would with an input.

Second component level

This is where ngx-sub-form is becoming useful. All you have to do is:

Add required providers using the utility function subformComponentProviders:

+import { subformComponentProviders } from 'ngx-sub-form';

@Component({
  selector: 'app-vehicle-listing',
  templateUrl: './vehicle-listing.component.html',
  styleUrls: ['./vehicle-listing.component.scss'],
+ providers: subformComponentProviders(VehicleListingComponent),
})
export class VehicleListingComponent {}

Make your original class extend NgxSubFormComponent or NgxSubFormRemapComponent if you need to remap the data (see below):

-import { subformComponentProviders } from 'ngx-sub-form';
+import { subformComponentProviders, NgxSubFormComponent } from 'ngx-sub-form';

-export class VehicleListingComponent {}
+export class VehicleListingComponent extends NgxSubFormComponent {}

Define the controls of your form:

protected formControls: Controls<VehicleListing> = {
  id: new FormControl(this.uuidService.generate(), Validators.required),
  price: new FormControl(null, Validators.required),
};

Simplified from the original example into src folder to keep the example as minimal as possible.

As you know, Angular reactive forms are not strongly typed. We're providing an interface (Controls<T>) to at least set the correct names within the form (but it will not help you when using form.get('...').value). It is still very useful and when making a refactor if your data structure changes and do not match the form structure Typescript compilation will fail.

The NgxFormComponent base class automatically extracts your form control names and exposes them as a public member on formControlNames

Then within the .html, you can reference them like so:

<fieldset [formGroup]="formGroup" class="container">
  <input type="text" placeholder="ID" [formControlName]="formControlNames.id" />

  <input type="number" placeholder="Price" [formControlName]="formControlNames.price" />
</fieldset>

Remapping Data

It is a frequent pattern to have the data that you're trying to modify in a format that is incovenient to the angular forms structural constraints. For this reason, ngx-form-component offers a separate extended class NgxSubFormRemapComponent which will require you to define two interfaces - one to model the data going in to the form (which will be applied internally as form.setValue()), and the other to describe the interface of the value that will be set on the form.

You're always better off making your data structure better suit Angular forms, than abusing forms to fit your data pattern

For a complete example of this see https://github.com/cloudnc/ngx-sub-form/blob/master/src/app/main/listing/vehicle-listing/vehicle-product.component.ts (repeated below):

interface OneVehicleForm {
  speeder: Speeder;
  spaceship: Spaceship;
  vehicleType: VehicleType;
}

@Component({
  selector: 'app-vehicle-product',
  templateUrl: './vehicle-product.component.html',
  styleUrls: ['./vehicle-product.component.scss'],
  providers: subformComponentProviders(VehicleProductComponent),
})
export class VehicleProductComponent extends NgxSubFormRemapComponent<OneVehicle, OneVehicleForm> {
  protected formControls: Controls<OneVehicleForm> = {
    speeder: new FormControl(null),
    spaceship: new FormControl(null),
    vehicleType: new FormControl(null, { validators: [Validators.required] }),
  };

  public VehicleType = VehicleType;

  protected transformToFormGroup(obj: OneVehicle): OneVehicleForm {
    return {
      speeder: obj.vehicleType === VehicleType.SPEEDER ? obj : null,
      spaceship: obj.vehicleType === VehicleType.SPACESHIP ? obj : null,
      vehicleType: obj.vehicleType,
    };
  }

  protected transformFromFormGroup(formValue: OneVehicleForm): OneVehicle {
    switch (formValue.vehicleType) {
      case VehicleType.SPEEDER:
        return formValue.speeder;
      case VehicleType.SPACESHIP:
        return formValue.spaceship;
    }
  }
}