The FormGroup Directive
Forms in Angular are one of those things that seem to work by some kind of magic. When working with Reactive Forms we would create Form Controls in some way, maybe through a Form Group:
loginForm = this.fb.nonNullable.group({
email: [''],
password: [''],
});
and then we somehow tie those controls to a standard <form>
DOM element:
<form>
<input type="email" placeholder="email"/>
<input type="password" placeholder="password" />
<button type="submit">Submit</button>
</form>
like this:
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" placeholder="email"/>
<input fromControlName="password" type="password" placeholder="password" />
<button type="submit">Submit</button>
</form>
But what’s the deal with this formGroup
property, and the formControlName
properties? Why can we use an ngSubmit
event binding? These are not things that exist on the standard <form>
DOM element, they come from Angular forms specifically. But… how exactly does that work?
Understanding this can help us to understand Angular forms in more detail, and as well as that it can teach us a lot about how the more advanced features of Angular work more generally.
The Power of a Directive
The secret to powering these forms are directives. We have already covered how directives provide us with a way to extend the behavior of existing components/elements by matching against a selector
.
Let’s focus on just the <form>
itself. By giving it a formGroup
property, we are going to trigger a match with the FormGroupDirective
:
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
exportAs: 'ngForm'
})
export class FormGroupDirective extends ControlContainer implements Form, OnChanges, OnDestroy {
As you can see, it has a selector
of [formGroup]
. This is not something we ever actually see - it is provided by Angular in the ReactiveFormsModule
. That means that if we import the ReactiveFormsModule
(which we need to do to use Reactive Forms), the FormGroupDirective
will become active and any time we use the formGroup
property it will attach this directive to the element.
This is how Angular adds all of its special features onto a standard <form>
DOM element.
The FormGroupDirective in Detail
We are going to dive a little more deeply into this, as we will actually be making use of these more advanced features in our next application build. To help us understand what is going on, I have created this little diagram:
As we just discussed, adding [formGroup]
to the <form>
element is what attaches the FormGroupDirective
to the element. But, we also provide this formGroup
with an input of myFormGroup
(or in the case of our login example: loginForm
).
If you were to inspect the source code for the FormGroupDirective you would see that it has an input:
@Input('formGroup') form: FormGroup = null!;
The input has the same name as the selector
, which means that this:
<form [formGroup]="myFormGroup">
Serves two purposes:
- It attaches the
FormGroupDirective
- It supplies our
FormGroup
as an input to the directive
Now the FormGroupDirective
can do whatever fancy stuff it needs with the FormGroup
we just passed to it.
Although we aren’t focusing on it here, the formControlName
attribute we are using uses the same general idea. Using formControlName
will attach this directive that is also provided by ReactiveForms
:
@Directive({selector: '[formControlName]', providers: [controlNameBinding]})
export class FormControlName extends NgControl implements OnChanges, OnDestroy {
Now, let’s get back to focusing on our diagram:
There is one final bit here that we have not discussed yet, and it is the concept we are going to be utilising in our next application:
<form [formGroup]="myFormGroup" #form="ngForm">
Notice that we are setting up a template variable called form
and we are assigning ngForm
to it. This looks like another bit of magic. The reason we specifically want to use it is that we want to check if the form has been submitted in order to display something or not.
Since we have a myFormGroup
that represents the form, you might think you would be able to do something like:
myFormGroup.submitted
But that functionality does not exist on the FormGroup
- it does not know whether or not the form has been submitted. However, our FormGroupDirective
which is attached to the <form>
element does know if the form has been submitted or not.
If we go back to the FormGroupDirective
:
@Directive({
selector: '[formGroup]',
providers: [formDirectiveProvider],
host: {'(submit)': 'onSubmit($event)', '(reset)': 'onReset()'},
exportAs: 'ngForm'
})
export class FormGroupDirective extends ControlContainer implements Form, OnChanges, OnDestroy {
We can see that it has this exportAs
property, and it is exporting itself as ngForm
. If we want to grab a reference to this directive, so that we can utilise some functionality from it, we can assign the name it exports itself as to our template variable:
<form [formGroup]="myFormGroup" #form="ngForm">
Which will then allow us to check properties on the FormGroupDirective
like submitted
:
<div *ngIf="form.submitted">
The form has been submitted
</div>
All of the stuff we have just covered isn’t stuff you strictly need to know in order to use Angular forms. We can just use them as instructed by the documentation and not worry about how it actually works behind the scenes. But, understanding stuff like this does make everything make more sense and also allows you to utilise these same powerful concepts in your own code if you ever find the need.