Creating a Form Modal Component
In this lesson, we are going to create another dumb/presentational component and it is going to be one that is also shared with multiple features. For the home feature that we are currently working on we need the ability to display a form inside of the modal we are launching to allow the user to create a new checklist.
I feel kind of bad because I told you that it would get easier after our
unusually difficult first component. That is true, but the form-modal
is
probably the second most difficult feature in the application. So again, we are
going to touch on some somewhat advanced concepts here. Don’t feel too worried
if things aren’t making complete sense.
We could just create a dumb component specifically for the home
feature, but we
are also going to need to do the exact same thing when we get to adding items to
individual checklists in the checklist
feature we will create later — we will
again need to display a form inside of a modal. We might decide to just manually
hard code forms for each of these features rather than having a single shared
form component, but since our forms are going to be so simple (we basically just
need to accept a single text input) it will be relatively easy to create
a single component that can be shared with both features.
Create the Form Modal Component
Now we will create our dumb/presentational form component.
Create a new file at
app/shared/ui/form-modal.component.ts
and add the following:
import { KeyValuePipe } from '@angular/common';
import { Component, input, output } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-form-modal',
template: `
<header>
<h2>{{ title() }}</h2>
<button (click)="close.emit()">close</button>
</header>
<section>
<form [formGroup]="formGroup()" (ngSubmit)="save.emit(); close.emit()">
@for (control of formGroup().controls | keyvalue; track control.key){
<div>
<label [for]="control.key">{{ control.key }}</label>
<input
[id]="control.key"
type="text"
[formControlName]="control.key"
/>
</div>
}
<button type="submit">Save</button>
</form>
</section>
`,
imports: [ReactiveFormsModule, KeyValuePipe],
})
export class FormModalComponent {
formGroup = input.required<FormGroup>();
title = input.required<string>();
save = output();
close = output();
}
This is the component in its entirety. It is a somewhat complex component, but also reasonably within the realms of the concepts we have been learning so far.
The only thing we haven’t actually seen yet here is the keyvalue
pipe which we
also add to the imports
array through KeyValuePipe
. The idea here is that
this component will be given a FormGroup
which contains form controls (e.g. we
might have a username
form control). The keyvalue
pipe will allow us to
access the key
and value
in these control objects. The idea is that we want
to use the key
, which is actually the name of the form control, and assign
that as the formControlName
for the input. In this way, the specific inputs we
are dynamically rendering out will be correctly associated with their
corresponding form control — that means updating the input field will update the
form control’s value.
That is the most complex part here. We will talk through the rest in just a moment, but this is a good opportunity to just take a look at the code and see if you can understand generally what is happening.
Again, don’t worry if it isn’t all making sense. There is nothing you need to do right now, just see what you can figure out about the code before moving on.
Click here to reveal solution
Solution
There is a bit going on here, so let’s talk through what is going on. Let’s start with the class:
export class FormModalComponent {
formGroup = input.required<FormGroup>();
title = input.required<string>();
save = output();
close = output();
}
Remember that this is a dumb component, so generally it is not going to inject any dependencies and it doesn’t know about anything that is happening in the broader application. It just gets its inputs, and sends outputs to communicate with whatever parent component is using it (the dumb child component doesn’t even know what component is using it).
In this case, we have two inputs. We want to be able to configure the title
to be displayed in the template, and we also allow the parent component to
supply a FormGroup
as an input. This is what will allow the parent component
to configure what form fields to display. We will render out an input in the
template for each control defined in the FormGroup
(using the technique we
talked about above).
We also have a save
output that is used to indicate to the parent
component when the save button has been clicked, and another output that is used
to indicate when the close button has been clicked. Let’s take a closer look at
the template now:
<header>
<h2>{{ title() }}</h2>
<button (click)="close.emit()">close</button>
</header>
<section>
<form [formGroup]="formGroup()" (ngSubmit)="save.emit(); close.emit()">
@for (control of formGroup().controls | keyvalue; track control.key){
<div>
<label [for]="control.key">{{ control.key }}</label>
<input
[id]="control.key"
type="text"
[formControlName]="control.key"
/>
</div>
}
<button type="submit">Save</button>
</form>
</section>
Angular hooks into the functionality of the standard HTML <form>
element,
which we can activate by binding our FormGroup
to it using the formGroup
directive (thanks to the ReactiveFormsModule
import):
<form [formGroup]="formGroup()" (ngSubmit)="save.emit(); close.emit()">
Remember how a @Directive
works by supplying a selector that determines what
it attaches to? This is exactly how Angular makes these forms work — it’s just
a directive that has a selector of [formGroup]
(i.e. it will attach to
anything that has the formGroup
attribute).
We also bind to the ngSubmit
event which is triggered when the form is
submitted. When this happens, we want to trigger both our save
and emit
events. Something to notice here is that we do not do this:
<form [formGroup]="formGroup()" (ngSubmit)="handleSubmit()">
We could create a separate method, and then in that method run the code we need. But, in general, we will generally try to avoid writing callback methods like this if we can. Triggering actions directly in the template, and keeping things neat, is going to require us to use a more declarative design.