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
. This is from the
Angular source code:
@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.