Which one to choose?
Reactive forms are great, we can dynamically create new controls, and validate them synchronously or asynchronously in very different ways. All of this can also be achieved with template driven forms, however, this would require more repetitive code.
On the other hand, if we want to add one control, e.g. a checkbox which hides/shows us a certain component, then creating a form, and then adding listeners is an excess of form over content. All you need is a ngModel with the ngModelChange attribute.
Therefore, the solution is to use both. Details of TDF and RF implementation are very well described in the documentation and also on the angular.love blog. In this article, I will focus on the combination of the two.
Template Driven Forms
I have worked with Angular since its first versions ( Angular 1.3 to be precise), so Template Driven Forms were the obvious approach for me when I switched to Angular 2. Well implemented, they work great on projects.
At this point, I should explain what “well implemented” actually means. Imagine you want to add a form in app.component.html:
1 2 3 4 |
<div class="form-input"> <label for="name">Name</label> <input type="text" id="name" [(ngModel)]="name"> </div> |
This isn’t a good implementation. What if we wanted to add several similar forms? A lot of duplicated code will be difficult to change in the future.
It’s much better to make a component out of it.
Let’s create the MyForms module with the input-text component in it (this is a simplification, therefore in a real project I would advise you to first create a library module, and then the form module).
We can add this component to the export (at the moment we only have one, but there will be more in the future).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const COMPONENTS = [ InputTextComponent ] @NgModule({ declarations: [ ...COMPONENTS ], exports: [ ...COMPONENTS ], imports: [ FormsModule, ReactiveFormsModule, ] }) export class MyFormsModule { } |
Now we try to use it in the app.component (remember to import MyModule).
1 |
<app-input-text label="name" name="name" [(ngModel)]="name"></app-input-text> |
I also added the name attribute, which will be useful later (when it will be embedded in the form).
Now you should only see the caption: input-text works!
That’s fine because after all, our component isn’t ready yet.
Let’s start with TS:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { Component, forwardRef } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @Component({ selector: 'app-input-text', templateUrl: './input-text.component.html', styleUrls: ['./input-text.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputTextComponent), multi: true, }, ], }) export class InputTextComponent implements ControlValueAccessor { @Input() label: string = ''; @Input() name: string = ''; value: string = ''; onChange: (_: any) => void = (_: any) => {}; onTouched: () => void = () => {}; constructor() {} updateChanges() { this.onChange(this.value); } writeValue(value: string): void { this.value = value; this.updateChanges(); } registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } } |
Let’s add the provider and the necessary ControlValueAccessor implementation.
Plus the label and name fields.
And HTML:
1 2 3 4 |
<div class="form-input"> <label [for]="name">{{label}}</label> <input type="text" [name]="name" [id]="name" [(ngModel)]="value" (ngModelChange)="updateChanges()"> </div> |
It now works perfectly and we can create new fields very easily.
1 2 |
<app-input-text label="name" name="name" [(ngModel)]="name"></app-input-text> <app-input-text label="phone" name="phone" [(ngModel)]="name"></app-input-text> |
If we want to change the appearance or expand it with new opportunities in the future, the changes made in one place will have an overall impact.
Reactive Forms
The advantage of reactive forms is the possibility of creating dynamic forms in a TS file. If you use Reactive Forms together with inputs:
1 2 3 4 |
<div class="form-input"> <label for="name">Name: </label> <input id="name" type="text" [formControl]="name"> </div> |
this is not the best approach. A form generator is definitely worth having. You can write it yourself or use the brilliant ngx-formly library.
We install and add the library according to https://formly.dev/guide/getting-started.
Important! We only need FormlyModule without FormlyBootstrapModule.
We create a new form-input-text component (our control which is based on input-text however, will be used in the form) and a second form-field (a wrapper for all our components, we only have one input-text at the moment, but in the future, it will probably be input-select, input-converter etc. Thanks to the wrapper, we don’t have to worry about styles such as margins or displaying validation notifications).
We import everything to MyForrms as a root.
We register our wrapper and our control in the root. We want to use the control as an “input” and we want to use our form-field wrapper on it.
1 2 3 4 |
FormlyModule.forRoot({ wrappers: [{ name: 'form-field', component: WrapperFormFieldComponent }], types: [{ name: 'input', component: FormInputTextComponent, wrappers: ['form-field'] }], }), |
Now we can deal with the input and wrapper code.
Wrapper HTML code:
1 2 3 |
<div class="form-input"> <ng-template #fieldComponent></ng-template> </div> |
So far it’s very simple, there is one class only, and indicated where to render the controls using the reference #fieldComponent. In the future, we will add e.g. error messages here.
We can get rid of <div class = “form-input”> from input-text.component, the class will be in our wrapper.At the moment the TS file only extends the FieldWrapper class.
1 2 3 4 5 6 7 8 9 10 |
import { Component } from '@angular/core'; import { FieldWrapper } from '@ngx-formly/core'; @Component({ selector: 'app-wrapper-form-field', templateUrl: './wrapper-form-field.component.html', styleUrls: ['./wrapper-form-field.component.scss'] }) export class WrapperFormFieldComponent extends FieldWrapper { } |
Similarly does the TS file for FormInputText.
1 2 3 4 5 6 7 8 9 10 11 |
import { Component } from '@angular/core'; import { FieldType } from '@ngx-formly/core'; @Component({ selector: 'app-form-input-text', templateUrl: './form-input-text.component.html', styleUrls: ['./form-input-text.component.scss'] }) export class FormInputTextComponent extends FieldType { } |
And in its HTML file we add:
1 |
<app-input-text type="input" [formControl]="formControl" [formlyAttributes]="field"></app-input-text> |
If you’re using newer versions of Angular, you have to change strictTemplates to false in tsconfig.json.
Everything is ready now. We only need to add FormlyModule to the export in MyForms.
After adding it, we can start creating our first control and enter it into the app.component.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
model = { name: null } form = new FormGroup({}); fields: FormlyFieldConfig[] = [ { key: 'name', type: 'input', templateOptions: { label: 'Name', }, }, ]; |
The library allows you to really do many things. The example above is very primitive. Key – the value will be put under this key, type is the control which will be displayed, and in templateOptions we transfer the label and various attributes.
Now we add to HTML:
1 2 3 |
<form [formGroup]="form"> <formly-form [model]="model" [fields]="fields" [form]="form"></formly-form> </form |
These are reactive forms, so the model is optional of course.
Where is the advantage?
Certainly, the combination of Template Driven Forms and Reactive Forms isn’t dedicated to small applications. The biggest benefit we have is when most of our forms are in Reactive Forms, but sometimes we need to add a control. For example, we want to add a switch which will reveal a new component to us. In the case of taking only the Reactive Forms approach, we would have to define the form, then spot its changes and remember to unsubscribe. In the case of Template Driven, the component just needs to be pasted, ngModelChange set and everything should work.
What’s next?
Our wrapper component isn’t just for HTML. Our module can certainly be improved by validation, with the result that an incorrect value will be displayed as an error. This is a topic for the next article :).
Link to the GitHub code: https://github.com/rograf/angular-forms-sample.
Leave a Reply