Quick reusable nested template-driven forms in Angular

Andrey Zakharov
10-12-2018

Angular framework provides us with a very nice way to build forms without necessity to write additional js code: template-driven forms. Once you use it, you love it, but as a good developer, you might build a form which you want to re-use. And this approach has one issue which I’m going to show you right away.

Sign me up

Let’s say we have a basic form to fill in:

<form #mainForm="ngForm">
    <input type="text" name="firstName" ngModel required>
    <input type="text" name="lastName" ngModel required>
</form>
<pre>
    {{ mainForm.value | json }}
</pre>
<p>
    Form is valid: {{ mainForm.valid }}
</p>

The amazing point here that this code works out of the box without ANY line of javascript. We have an access to the form object inside template, so we can watch the current value of form and see other properties, like validation. If you insert this code into your angular app and play with inputs, you’ll see something like that:

{
"firstName": "Andrey",
"lastName": "Zakharov"
}

Form is valid: true

I don’t know where you are, but I’ll find you and I’ll register you…

Cool, pretty usable, but after a while, we decided to extend our sign-up form with new fields:

<form #mainForm="ngForm">
    <input type="text" name="firstName" ngModel required>
    <input type="text" name="lastName" ngModel required>
    <fieldset ngModelGroup="address">
         <input type="text" name="street" ngModel required>
         <input type="number" name="house" ngModel required>
    </fieldset>
</form>

ngModelGroup directive provides us an easy way to insert nested object into our form. So, in this case mainForm.value | json will show something like that:

{
"firstName": "Andrey",
"lastName": "Zakharov",
"address": {
"street": "Street name",
"house": 12
}
}

Pretty powerful, right? We can make a pretty complex form in one minute. After another round of delivering cool features, we noticed that we copy paste address object into different forms, for example our shop cart wants to know the delivery address. So it’s time to move this part of code into separate component.

And here we got a problem: if we just move fieldset piece of code into another component and try to insert it into our main form, our “address” object just disappears. NgForm can not see that we have something related to it in child. Possible solutions in google say that you might define form in reactive way and then add nested controls on init or pass parent form object into child as an input, so child component can inject itself into parent. I consider both solutions as non-acceptable because they create additional coupling between components, parent and children start knowing too much of each other. Of course, you can keep doing it, but after certain time you will be tired. And then…

…and I will decouple you

Then I found very interesting feature of framework, which provides us a way to re-use partial forms whenever we need it without any coupling. This is just a one line of code, so I can’t say anything except showing it right away. Let’s say we prepared our non-working address form as a separate component and it looks like this:

@Component({
    selector: 'address-form',
    template: '
        <fieldset ngModelGroup="address">
            <input type="text" name="street" ngModel required>
            <input type="number" name="house" ngModel required>
        </fieldset>
    ',
})
export class AddressFormComponent {}

Only thing we need to do in a way to make it work, is to define viewProviders in @Component decorator:

@Component({
    // ...previous code...
    viewProviders: [ { provide: ControlContainer, useExisting: NgForm } ],
})

And our form works perfect. Read more about viewProviders in angular documentation here

LEAVE A REPLY

4 Comments

  • Laszlo says:

    Great article.
    I have a question please, ho can we validate multiply nested template driven form?
    For example, we have a main form with a Tab sheet with 3 tabs. Each tab contains a component, which contains more data input fields. some field is required. I would like to reach each component on the tabs participate in the main form of validation.

    • Andrey Zakharov says:

      Hi Laszlo, I think you can consider every tab not as an independent form, but as a form group (ngModelGroup in the article). So you will end up with smth like:

      <form #mainForm>
      <tab/>
      <tab/>
      <tab/>
      </form>

      – where every tab component is a child component with ngModelGroup and viewProvider injection. Then validation is automatically assigned to the mainForm form.

      Of course, in this case, you shouldn’t wrap tabs with ngIf, because it will remove non-active tabs from DOM and angular will stop watching it, so you have to keep all nested components in DOM and hide non-active tabs by css.

  • Daniel says:

    Many thanks, great article, really helped

  • Galina says:

    Many thanks, helped a lot! Short and clear;)

you might also like