Package Exports
- ngx-sub-form
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ngx-sub-form) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
NgxSubForm
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: ngx-sub-form
Demo
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.
If you want to see the demo in action, please visit https://cloudnc.github.io/ngx-sub-form.
Setup
ngx-sub-form
only provides
- 2 classes:
NgxSubFormComponent
,NgxSubFormRemapComponent
- 2 interfaces:
Controls<T>
,ControlsNames<T>
- 1 function:
subformComponentProviders
So there's nothing to setup (like a module), you can just use them directly.
Usage
When can I use ngx-sub-form
?
- When you create a simple form, it'll give you better typings
- When you want to create a bigger form that you need to split up into sub components
- When dealing with polymorphic data that you want to display in a form
Type safety you said?
When extending NgxSubFormComponent
or NgxSubFormRemapComponent
you'll have access to the following properties (within .ts
and .html
):
formGroup
: The actual form group, useful to define the binding[formGroup]="formGroup"
into the viewformControlNames
: All the control names available in your form. Use it when defining aformControlName
like that<input [formControlName]="formControlNames.yourControl">
formGroupControls
: All the controls of your form, helpful to avoid doingformGroup.get(formControlNames.yourControl)
, instead just doformGroupControls.yourControl
formGroupValues
: Access all the values of your form directly without doingformGroup.get(formControlNames.yourControl).value
, instead just doformGroupValues.yourControl
(and it'll be correctly typed!)formGroupErrors
: All the errors of the current form including the sub errors (if any), just useformGroupErrors
orformGroupErrors?.yourControl
. Notice the question mark informGroupErrors?.yourControl
, it will returnnull
if there's no error
With AOT turned on you'll get proper type checking within your TS and HTML files. When refactoring your interfaces your form will error if a property should no longer be here or if one is missing.
First component level
Within the component where the (top) form will be handled, you have to define the top level structure. You can do it manually as you'd usually do, but it's better to extend from NgxSubFormComponent
as you'll get some type safety. If dealing with polymorphic data, each type must have it's own form control:
(even if it doesn't match your model, we'll talk about that later)
enum ListingType {
VEHICLE = 'Vehicle',
DROID = 'Droid',
}
interface OneListingForm {
title: string;
price: number;
// polymorphic form where product can either be a vehicle or a droid
listingType: ListingType | null;
vehicleProduct: OneVehicle | null;
droidProduct: OneDroid | null;
}
@Component({
selector: 'app-listing',
templateUrl: './listing.component.html',
styleUrls: ['./listing.component.scss'],
})
export class ListingComponent extends NgxSubFormComponent<OneListingForm> {
// to access it from the view
public ListingType = ListingType;
protected getFormControls(): Controls<OneListingForm> {
return {
vehicleProduct: new FormControl(null),
droidProduct: new FormControl(null),
listingType: new FormControl(null, Validators.required),
id: new FormControl(null, Validators.required),
title: new FormControl(null, Validators.required),
imageUrl: new FormControl(null, Validators.required),
price: new FormControl(null, Validators.required),
};
}
}
Then, within the .component.html
we create a select
tag to choose between the 2 types:
<select [formControlName]="formControlNames.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]="formGroup">
<div [ngSwitch]="formGroupValues.listingType">
<app-droid-listing
*ngSwitchCase="ListingType.DROID"
[formControlName]="formControlNames.droidProduct"
></app-droid-listing>
<app-vehicle-listing
*ngSwitchCase="ListingType.VEHICLE"
[formControlName]="formControlNames.vehicleProduct"
></app-vehicle-listing>
</div>
</form>
One thing to notice above:
<app-droid-listing>
and<app-vehicle-listing>
are customControlValueAccessor
s and let us bind them toformControlName
as we would with an input.
Second component level
This is where ngx-sub-form
is becoming (more) 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
orNgxSubFormRemapComponent
if you need to remap the data (will be explained later):
+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 extends NgxSubFormComponent {}
Define the controls of your form (as we previously did in the top form component):
export class VehicleProductComponent extends NgxSubFormComponent<OneVehicleForm> {
protected getFormControls(): Controls<VehicleListing> {
return {
speeder: new FormControl(null),
spaceship: new FormControl(null),
vehicleType: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Simplified from the original example into src folder to keep the example as minimal and relevant as possible.
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 class NgxSubFormRemapComponent
which will require you to define two interfaces:
- One to model the data going into the form
- The other to describe the data that will be set as the value
Example, take a look into VehicleProductComponent
:
// merged few files together to make it easier to follow
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;
interface OneVehicleForm {
speeder: Speeder | null;
spaceship: Spaceship | null;
vehicleType: VehicleType | null;
}
@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> {
public VehicleType = VehicleType;
protected getFormControls(): Controls<OneVehicleForm> {
return {
speeder: new FormControl(null),
spaceship: new FormControl(null),
vehicleType: new FormControl(null, { validators: [Validators.required] }),
};
}
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 | null {
switch (formValue.vehicleType) {
case VehicleType.SPEEDER:
return formValue.speeder;
case VehicleType.SPACESHIP:
return formValue.spaceship;
case null:
return null;
default:
throw new UnreachableCase(formValue.vehicleType);
}
}
}
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;
}
}
}
Our "incoming" object is of type OneVehicle
but into that component we treat it as a OneVehicleForm
to split the vehicle (either a speeder
or spaceship
) in 2 separated properties.
Helpers
onFormUpdate
hook: Allows you to react whenever the form is being modified. Instead of subscribing tothis.formGroup.valueChanges
orthis.formControls.someProp.valueChanges
you will not have to deal with anything asynchronous nor have to worry about subscriptions and memory leaks. Just implement the methodonFormUpdate(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 parentsetValue
orpatchValue
this method won't be triggeredgetFormGroupControlOptions
hook: Defines control options for construction of the internal FormGroup. Use this to define form-level validators
e.g.
interface PasswordForm {
password: string;
passwordRepeat: string;
}
class PasswordSubComponent extends NgxSubFormComponent<PasswordForm> {
protected getFormControls() {
return {
password: new FormControl(null, [Validators.required, Validators.minLength(8)]),
passwordRepeat: new FormControl(null, Validators.required),
};
}
public getFormGroupControlOptions(): FormGroupOptions<PasswordForm> {
return {
validators: [
formGroup => {
if (formGroup.value.password !== formGroup.value.passwordRepeat) {
return {
passwordsMustMatch: true,
};
}
return null;
},
],
};
}
}
Errors are exposed under the key errors.formGroup
e.g.
<input type="text" placeholder="Password" [formControlName]="formControlNames.password" />
<mat-error *ngIf="formControlErrors?.password?.minlength">Password too short</mat-error>
<input type="text" placeholder="Repeat Password" [formControlName]="formControlNames.passwordRepeat" />
<mat-error *ngIf="formControlErrors?.formGroup?.passwordsMustMatch">Passwords do not match</mat-error>
Be aware of
There's currently a weird behavior issue (?) when checking for form validity.
CF that issue and that comment.
It is also detailed into listing.component.html
.
Contribution
Please, feel free to contribute to ngx-sub-form
.
We've done our best to come up with a solution that helped us and our own needs when dealing with forms. But we might have forgotten some use cases that might be worth implementing in the core or the lib rather than on every project.
Remember that contributing doesn't necessarily mean to make a pull request, you can raise an issue, edit the documentation (readme), etc.