DEV Community

Cover image for How to Combine Add and Edit Forms in Angular
Michael Karén for This is Angular

Posted on • Originally published at michael-karen.Medium

How to Combine Add and Edit Forms in Angular

This article demonstrates how to build a reusable form component that supports add and edit modes. We do this by using the container and presentation components architecture. The goal is not about teaching forms, so we keep it as simple as possible using reactive forms.


The form

We create a simplified form without any validation to add and edit medals. The medal has three properties:

export interface Medal {
  name: string;
  type: string;
  sport: string;
}
Enter fullscreen mode Exit fullscreen mode

With reactive forms, the [formGroup] is a directive that we bind to and pass the form object in:

<h1 *ngIf="!medal">Add Medal</h1>
<h1 *ngIf="medal">Edit Medal</h1>

<form [formGroup]="form" (ngSubmit)="submit()">
  <label>Name</label>
  <input type="text" formControlName="name" /><br />

  <label>Type</label>
  <input type="text" formControlName="type" /><br />

  <label>Sport</label>
  <input type="text" formControlName="sport" /><br />

  <button type="submit">Submit</button>
</form>
Enter fullscreen mode Exit fullscreen mode

We inject the FormBuilder service and use the group() method to create the form controls matching the template:

import { 
  ChangeDetectionStrategy, Component, EventEmitter, 
  Input, OnInit, OnChanges, Output, SimpleChanges
} from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Medal } from '../app.component';

@Component({
  selector: 'medal-form',
  templateUrl: 'medal-form.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MedalFormComponent implements OnInit, OnChanges {
  @Input() medal: Medal;
  @Output() submitted = new EventEmitter<Medal>();
  form: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.form = this.fb.group({
      name: [''],
      type: [null],
      sport: [null],
    });
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes.medal?.currentValue) {
      this.form?.patchValue(this.medal);
    }
  }

  submit() {
    this.submitted.emit(this.form.getRawValue());
    this.form.reset();
  }
}
Enter fullscreen mode Exit fullscreen mode

We use the Input() decorator for the medal property. Then we use it when we have data that we can send to the child component. To watch for changes on an Input() property, we use the OnChanges lifecycle hook. Whenever it detects changes to the medal property, we populate the form with patchValue(). When submit is pressed, we emit the form values through the Output() property submitted.

We have implemented our reusable form component as a dumb component. Now let us talk more about the architecture we chose and how we use the form component we created.


The problem area

Let us first consider why we want to split into these two components. When using only one, we need to subscribe() to the observable data that Angular is keen for us to use. There are disadvantages to manually subscribing to observables. Some can lead to bugs that can be hard to debug. Using subscribe() also requires us to unsubscribe at the end of the component lifecycle to avoid memory leaks.

Subscribing to the observable manually in the ngOnInit() doesn't always work with the preferred OnPush change detection strategy out of the box. We sometimes need to tell Angular change detection manually when we want it to run. Needless to say, when someone comes to me for help with code where the data is not updating for some reason, the first thing I do is look for a subscribe() in the code.

Async pipe to the rescue?

The next, better solution is to use the async pipe. But, this also has some downsides. Objects have to be unwrapped, sometimes multiple times, in the template using *ngIf="data$ | async as data"

Properties unwrapped using *ngIf or *ngFor are not accessible in the component's methods. We have to pass these properties to the methods from the template as method parameters which makes the code harder to read. And let me not get started about the testing.

So how can we then solve this better?


Smart/dumb components

For a better architecture, we split components into two types of specialized components:

  • Smart components: also known as container components.
  • Dumb components: also known as presentation components.

The responsibility of the dumb component is to present the data, while the smart one is responsible for fetching and managing the data. Presentation components should be child components of the container components on your page.

Interaction between smart and dumb components is done by:

  • Input -presentation component receives data from parent
  • Output -presentation component triggers actions that parent listens to

By doing this, the presentation component remains isolated from the parent container component via a clearly defined interface.

Using the form

For the final piece of the puzzle, here are the ways we can use our presentational form as a create form:

<medal-form
  (submitted)="onSubmitted($event)"
></medal-form>
Enter fullscreen mode Exit fullscreen mode

We don't send any data in, so we get an empty form from which we get notified when it is submitted. Then the only thing left for us to do is call the backend through our store or service.

In the edit form we fetch the data and send it into the form through an async pipe:

<medal-form 
  [medal]="medal$ | async"
  (submitted)="onSubmitted($event)"
></medal-form>
Enter fullscreen mode Exit fullscreen mode

Now, we let the framework handle the subscriptions for us. The presentational component manages the data as objects instead of observables.

I have created a playground for you to play with the code. There is no data fetching or async pipe used, but it gives you an idea of how it works.

Conclusion

In this article, we combined two forms by making a presentational component. When we send it data with an async pipe, it receives the unwrapped object and populates the form with it. This architecture gives us a cleaner, more robust solution that hopefully keeps the bugs away.

Top comments (5)

Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp

Wonderful! First time I see somebody applying the container/presentational pattern to forms.

I do it myself all the time, but felt a little bit lonely.

Congratulations!

Collapse
 
melcor76 profile image
Michael Karén

Thanks @rainerhahnekamp !

I find that the container/presenter pattern fits like a glove in the Angular framework and I try to get my team to use it as much as possible.

Collapse
 
alexandre_52d530255ff89c1 profile image
Alexandre

If I have a select input and I need to fetch options data from the backend, should I fetch them on the dumb or the smart component ?

Collapse
 
wojtrawi profile image
Wojciech Trawiński

Using a setter for form data Input is an option worth to consider.

Collapse
 
melcor76 profile image
Michael Karén

Yes, I kind of go back and forth using setters and ngOnChanges.
Right now I'm on team OnChanges. :)