Context: In the Angular framework there are two types of forms, template driven and reactive. Both of them are effective for their respective use cases but neither addresses the problem of reusability and logic encapsulation. This flaw results in very bulky and complex form components.
Objective: Angular Lazy Forms is a project that aims to deliver IoC solution for creating Reactive Forms in Angular. The goal behind the idea is to enable user creation of small loosely coupled components which can be used to create complex forms.
And import LazyFormsModule module in your app.module.ts:
imports: [
...
LazyFormsModule,
],
Demo and Tutorial
Following demo application uses NgLazyForms to recreates application from Angular Reactive Forms tutorial as for version 5.2.9 with minor changes that are discussed in the tutorial. To inspect demo for yourself simply clone repository and run:
npm i
ng serve
Recreation steps are presented below. If somewhere in the tutorial appears a path or a file that does not exist yet it means that it should be created.
Setup
Create a new project named angular-lazy-forms:
ng new angular-lazy-forms
Install NgLazyForms package:
npm i ng-lazy-forms
Vocabulary
NgLazyForms use LazyMetadatas to generate and manage components called LazyControls.
Component that implements LazyControl is refered as CustomLazyControl.
Its metadata (which implements LazyMetadata) is refered as CustomLazyMetadata.
LazyMetadata
NgLazyForms define LazyMetadata as well as getter and setter:
NgLazyForms define LazyControl as well as optional OnLazySetup interface:
exportabstractclassLazyControlComponent{
abstractvalue:any;
abstractmetadata:LazyMetadata;
abstractcontrol:AbstractControl;
}
exportinterfaceOnLazySetup{
onLazySetup();
}
Implementing OnLazySetup is advised as it allows a component to be rebuild without destroying it. This method is responsible for all the cleaning and set up of the component. Its functionality resembles quite a bit Angular's own ngOnChanges method. It is up to developer to provide it.
CustomLazyControl with CustomLazyMetadata
CustomLazyControl always comes with its own CustomLazyMetadata. It uses this metadata to build and configure itself. They are strongly coupled:
CustomLazyControl expects its CustomLazyMetadata as an input.
CustomLazyMetadata keeps a reference to its CustomLazyControl to tell NgLazyForms what component to create.
It is advised to keep them in the same file to avoid circular dependency warning as they both refer each other. Example of such a relation:
If this seems overwhelming at the moment don't worry. It will become clearer with examples.
Create a BaseMetadata
NgLazyForms define LazyMetadata but it is advised to expand it so it can be used it the application. Following example is just a suggestion that should be adjusted for needs of every application.
BaseMetadat class extends LazyMetadata by adding properties used by CustomLazyControls to build and customize itself. In this example, it is only the "label" but it could include "hint", "placeholder", "validators" etc.
MetadataAccessor wraps getLazyMetadata method to provide easier access to metadata.
metadata function simply wraps setLazyMetadata function shielding it from the rest of the application.
Create CustomLazyControls
The goal behind the idea is to enable user creation of small loosely coupled components which can be used to create complex forms.
Those "small loosely coupled components" are conventionally called CustomLazyControls. They should be small, reusable components that implement LazyControl. They depend on CustomLazyMetadata to build and customize itself.
Both CustomLazyControls and CustomLazyMetadatas do not ship with that library, it is up to the developer to create and maintain them.
In this tutorial, the Reactive Forms tutorial application is being recreated using NgLazyForms with exception of "Superpower" and "Sidekick". They are made using standard Reactive Forms to demonstrate that they can be used alongside NgLazyForms.
This means that following CustomLazyControls are required:
DefaultControl - to display standard text/number controls.
SelectControl - to display drop-down control.
AddressControl - to display 4 encapsulated control (street, city, state, zip code).
AddressArrayControl - to manage an array of addresses.
LazyControlsModule
First create LazyControlsModule, src/app/lazy-controls/lazy-controls.module.ts:
import{CommonModule}from'@angular/common';
import{NgModule}from'@angular/core';
import{ReactiveFormsModule}from'@angular/forms';
import{LazyFormsModule}from'ng-lazy-forms';
@NgModule({
imports:[
CommonModule,
ReactiveFormsModule,
LazyFormsModule,
]
declarations: [
],
entryComponents:[
]
})
exportclassLazyControlsModule{}
Created CustomLazyControls must be added to both declarations and entryComponents arrays.
This file exports DefaultControl and DefaultMetadata.
DefaultMetadata extends BaseMetadata and by extention LazyMetadata. This means that the contract is fulfilled and DefaultMetadata in fact derives from LazyMetadata.
The type field refers to html input type with default of "text".
The component field keeps a reference to the DefaultControl. It tells NgLazyForms that for DefaultMetadata the DefaultControl should be rendered.
DefaultControl implements LazyControl by introducing these fields:
The value is expected to be a string, number or a Date.
The metadata should be of its own DefaultMetadata type so that so that DefaultControl can use fields introduced by it (i.e. type).
The control is a FormControl because it renders a single control (not an array or an object).
onLazySetup method is very simple in this case. It creates a form every time this control is being built or rebuilt.
The metadata.label is used to place a label of a control.
The control is passed to formControl of an input.
The metadata.type is used to define a type of a control.
This way created DefaultControl is configurable and reusable. It is capable of displaying input for text, number and Date using configuration from DefaultMetadata.
All that remains is to add newly created DefaultControl to LazyControlsModule:
<option *ngFor="let option of metadata.collection" [value]="option">{{option}}</option>
</select>
</label>
</div>
This Control is very similar to DefaultControl with a difference that SelectMetadata defines a collection field which is used in the template to display select options.
DefaultControl and SelectControl could be merged using type field of DefaultMetadata similar to the way it is done in Angular Dynamic Forms tutorial. It is up to the developer to make the decision what is the best design choice for given application.
Now, add newly created SelectControl to LazyControlsModule:
At the moment there is no Address class created. It will be added later in this tutorial.
AddressControl is different as it creates FormGroup instead of FormControl. It also accepts an object as an input, not primitive. However, the biggest change is in the template.
lazyForm directive informs that this element is a container for CustomLazyControls.
formGroup takes as an input parent element. It can be either FormGroup or FormArray.
This means that every lazy-selector (CustomLazyControl) within lazyForm container will be attached to the parent element.
lazy-selector is a component responsible for rendering correct component based on the CustomLazyMetadata passed. It takes two mandatory inputs value and metadata. It is also responsible for attaching itself to the parent element.
Now, add newly created AddressControl to LazyControlsModule:
AddressArrayMetadata contains child field of type AddressMetadata used to render elements of an array.
AddressArrayControl is responsible for managing an array. Besides standard createForm method it defines two other, addItem and removeItem. They are very simple and operate on value, not control. value itself is shielded from outside the component. This means that any changes to that field in here have no effect on original value.
extends MetadataAccessor to be able to access metadata through metadata(propertyKey: string): BaseMetadata; method.
defines update method so that we can easily assign new values.
@metadata attributes defined in src/app/lazy-controls/metadata.ts allows for the assignment of metadata to given fields. key is assigned automatically and is always the same as a field name.
Every field has a defined label that will be used in their respective CustomLazyControls.
state field has a defined collection which will be used to present options in SelectControl.
HeroDetailComponent is simplified compared to the one in Reactive Forms tutorial. Form create, recreate, and revert operations are done in the same way using createForm method. There is no need for a deep copy of form addresses because in hero.update(...) method an array is recreated which server as a deep copy.
In ngOnChanges() method it is required to use setTimeout with no latency. This is because cycle must end before resetting the form. This may change in the future updates.
There is no logging of the hero name in this tutorial because NgLazyForms are not designed to work with something like that. The idea behind NgLazyForms is to have a clearer separation of concerns. Parent component (e.g. HeroDetailComponent) is supposed to orchestrate a form as a whole, not particular elements. If there is a need to listen for a particular CustomLazyControl it should be done within that CustomLazyControl.
<inputtype="checkbox"formControlName="sidekick">I have a sidekick.
</label>
</div>
</form>
<p>heroForm value: {{ heroForm.value | json}}</p>
There are only two NgLazyForms specific elements in this template:
lazyForm directive.
two lazy-selector elements.
As discussed at the beginning of this tutorial sidekick and power remain as classic Reactive Forms elements to show they can be used alongside NgLazyForms.
It adds ValidatorsMetadata class which introduces required field as well as get validators(): ValidatorFn[] method that creates Angular validators array. Other validators can be added as well using this technique.