3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 2

The FormGroup Directive

EXTENDED

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:

FormGroupDirective

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:

FormGroupDirective

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.