3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 5

Creating a Form Modal Component

A reusable form for our modal component

STANDARD

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, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';

@Component({
  standalone: true,
  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 {
  @Input({ required: true }) formGroup!: FormGroup;
  @Input({ required: true }) title!: string;
  @Output() save = new EventEmitter<void>();
  @Output() close = new EventEmitter<void>();
}

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.

There is a bit going on here, so let’s talk through what is going on. Let’s start with the class:

  @Input({ required: true }) formGroup!: FormGroup;
  @Input({ required: true }) title!: string;
  @Output() save = new EventEmitter<void>();
  @Output() close = new EventEmitter<void>();

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.

STANDARD
Key

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).