Creating a Form Modal Component
In this lesson, we are going to create our first dumb/presentational component and it is going to be one that is shared with multiple features. For the home feature that we are currently working on we need to ability to display a form inside of the modal we are launching to allow the user to create a new checklist.
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 - we will again need to display a form inside of a modal. We might decide to just manually hardcode 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 a Checklist Interface
First, we are going to define precisely what a Checklist
is in our application by creating an interface.
Create a file at
app/shared/interfaces/checklist.ts
and add the following:
export interface Checklist {
id: string;
title: string;
}
Our checklist is quite simple, all we need is a title
and an id
. The purpose of the id
is to uniquely identify a particular checklists which will allow us to do things like select one specific checklist from a service or associate checklist items with a particular checklist.
Notice that we are breaking our typical convention of folder types here by creating an interfaces
folder. As I mentioned before, it is more common to break the typical data-access
/ui
/utils
convention within the shared
folder. We could place our interfaces
inside of data-access
but personally I like having a dedicated folder for the models/interfaces in the application.
Create the Form Modal Component
Now we will create our dumb/presentational form component, also without our shared
folder.
Create a new file at
app/shared/ui/form-modal.component.ts
and add the following:
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
NgModule,
Output,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
@Component({
selector: 'app-form-modal',
template: `
<ion-header>
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form [formGroup]="formGroup" (ngSubmit)="handleSave()">
<ion-item *ngFor="let control of formGroup.controls | keyvalue">
<ion-label position="stacked">{{ control.key }}</ion-label>
<ion-input type="text" [formControlName]="control.key"></ion-input>
</ion-item>
<ion-button
color="dark"
expand="full"
type="submit"
[disabled]="!formGroup.valid"
>
<ion-icon slot="start" name="save-outline"></ion-icon> Save
</ion-button>
</form>
</ion-content>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormModalComponent {
@Input() title!: string;
@Input() formGroup!: FormGroup;
@Output() save = new EventEmitter<boolean>();
handleSave() {
this.save.emit(true);
}
}
@NgModule({
imports: [CommonModule, ReactiveFormsModule, IonicModule],
declarations: [FormModalComponent],
exports: [FormModalComponent],
})
export class FormModalComponentModule {}
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 {
@Input() title!: string;
@Input() formGroup!: FormGroup;
@Output() save = new EventEmitter<boolean>();
handleSave() {
this.save.emit(true);
}
}
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
.
We also have a single save
output that is used to indicate to the parent component when the save button has been clicked. Let’s take a closer look at the template now:
<ion-header>
<ion-toolbar>
<ion-title>{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<form [formGroup]="formGroup" (ngSubmit)="handleSave()">
<ion-item *ngFor="let control of formGroup.controls | keyvalue">
<ion-label position="stacked">{{ control.key }}</ion-label>
<ion-input type="text" [formControlName]="control.key"></ion-input>
</ion-item>
<ion-button
color="dark"
expand="full"
type="submit"
[disabled]="!formGroup.valid"
>
<ion-icon slot="start" name="save-outline"></ion-icon> Save
</ion-button>
</form>
</ion-content>
Most of this is just the standard structure of an Ionic page, but there are a few interesting things going on. 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: