85

I want to create a single big form with angular 2. But I want to create this form with multiple components as the following example shows.

App component

<form novalidate #form1="ngForm" [formGroup]="myForm">
<div>
    <address></address>
</div>
<div>
    <input type="text" ngModel required/>
</div>

<input type="submit" [disabled]="!form1.form.valid" > </form>

Address component

<div>
<input type="text" ngModel required/> </div>

When I use the code above it was visible in the browser as i needed but the submit button was not disabled when I delete the text in address component.
But the button was disabled correctly when I delete the text in input box in app component.

Kamil Naja
  • 6,267
  • 6
  • 33
  • 47
Lasitha Yapa
  • 4,309
  • 8
  • 38
  • 57
  • What does your address component look like? Is it a ControlValueAccessor? – Ján Halaša Apr 07 '17 at 06:00
  • Just a simple component without anything in the class body – Lasitha Yapa Apr 07 '17 at 06:04
  • Try adding this to the bottom of the template: {{ form1.value | json }} And see if that contains both input elements or only one. I know that a form cannot be split into components loaded with the router. It is also possible that it cannot "find" items in a nested component either. – DeborahK Apr 07 '17 at 06:10
  • @DeborahK Yes u are correct elements from address component is not shown in the json. Is there any way to achieve this? – Lasitha Yapa Apr 07 '17 at 06:19
  • 1
    Although AJT82's answer is great and very helpful, it's a solution with an other approach than asked for. I put a solution how to achieve a form over multiple components [here:](https://stackoverflow.com/a/53118308/4636870) (further down). – dannybucks Jan 14 '19 at 07:17

6 Answers6

130

I would use a reactive form which works quite nicely, and as to your comment:

Is there any other simple example for this one? Maybe the same example without loops

I can give you an example. All you need to do, is to nest a FormGroup and pass that on to the child.

Let's say your form looks like this, and you want to pass address formgroup to child:

ngOnInit() {
  this.myForm = this.fb.group({
    name: [''],
    address: this.fb.group({ // create nested formgroup to pass to child
      street: [''],
      zip: ['']
    })
  })
}

Then in your parent, just pass the nested formgroup:

<address [address]="myForm.get('address')"></address>

In your child, use @Input for the nested formgroup:

@Input() address: FormGroup;

And in your template use [formGroup]:

<div [formGroup]="address">
  <input formControlName="street">
  <input formControlName="zip">
</div>

If you do not want to create an actual nested formgroup, you don't need to do that, you can just then pass the parent form to the child, so if your form looks like:

this.myForm = this.fb.group({
  name: [''],
  street: [''],
  zip: ['']
})

you can pass whatever controls you want. Using the same example as above, we would only like to show street and zip, the child component stays the same, but the child tag in template would then look like:

<address [address]="myForm"></address>

Here's a

Demo of first option, here's the second Demo

More info here about nested model-driven forms.

AT82
  • 71,416
  • 24
  • 140
  • 167
  • This method would require you to duplicate the same `FormGroup` definition from the child in the parent. In your example above, the parent declares **address** with fields **street n zip** however, the address component also have the same **FormGroup** definition (i.e. street and zip) unless I am misunderstanding your solution. If there are many levels of nesting and forms are complex then this duplication is of child code into parent become messy. The approach to avoid it is to pass parent form to child component, I am not an expert on that :( still learning. – Raf Sep 29 '17 at 03:01
  • How can i split the component without creating a formGroup for address – Renil Babu Nov 22 '17 at 13:03
  • 1
    @RenilBabu You can pass the whole form to the child component. – AT82 Nov 22 '17 at 18:06
  • Its not working...Throwing error to create a formGroup instead and use that in the child – Renil Babu Nov 24 '17 at 04:42
  • @RenilBabu, it should work, could you create a demo for this and I'd be happy to take a look :) – AT82 Nov 24 '17 at 06:39
  • Hi @Alex, I just posted a question here https://stackoverflow.com/questions/48843445/cannot-read-property-of-undefined-passing-formgroup-to-child-component-angular regarding my troubles following the above example. if you have time would you please be able to have a look? – David Feb 17 '18 at 16:33
  • Hi @AJT82 Thank you so much for detailed explanation along with cod demo :) – Vaibhav Miniyar Apr 02 '20 at 11:25
20

There is a way to do that in template driven forms too. ngModel creates automatically a separate form on each component, but you can inject the form of the parent component by adding this to your component:

@Component({
viewProviders: [{ provide: ControlContainer, useExisting: NgForm}]
}) export class ChildComponent

You have to make sure though, that each input has a unique name. So if you use *ngFor to call your child component, you have to put the index (or any other unique identifier) into the name , e.g.:

[name]="'address_' + i"

If you want to structure your form into FormGroups, you use ngModelGroup and

viewProviders: [{ provide: ControlContainer, useExisting: NgModelGroup }]

instead of ngForm and add [ngModelGroup]="yourNameHere" to some of your child components html containing tags.

Keammoort
  • 3,075
  • 15
  • 20
dannybucks
  • 2,217
  • 2
  • 18
  • 24
  • This works but I can not figure out how to set up tests when using viewProvider. Would you be able to show a test example? – Jared Christensen Jan 21 '20 at 18:52
  • I haven't used tests with viewProviders yet, so I'm afraid I can't help you. However, should you figure that out, it would be great if you share it! – dannybucks Jan 22 '20 at 06:46
10

From my experience, this kind of form field composition is hard to make with template-driven forms. The fields embedded in your address component don't get registered in the form (NgForm.controls object), so they are not considered when validating the form.

  • You can create a ControlValueAccessor component (that accepts ngModel attribute) with all validations, but then it's hard to display validation errors and propagate changes (address is considered as a single form field with a complex value).
  • You could probably pass the form reference into the Address component and register your inner controls in it, but I haven't tried that and seems to be an odd approach (I haven't seen it anywhere).
  • You can switch to reactive forms (instead of template driven), pass a form group object (representing an address) into the Address component, keeping the validation in your form definition. You can see an example here https://scotch.io/tutorials/how-to-build-nested-model-driven-forms-in-angular-2
Ján Halaša
  • 8,167
  • 1
  • 36
  • 36
  • Thank you Jan. I had a look at that link before posting this question. But I can't understand what is happening clearly as it is combined with a *ngFor loop. Is there any other simple example for this one? Maybe the same example without loops. – Lasitha Yapa Apr 07 '17 at 06:35
  • 1
    All examples I found use arrays and *ngFor, since it's probably the most common use-case for reusing form parts. But I think it doesn't affect the solution - just don't index the form group you want to get into the Address component and skip the formArray parts. – Ján Halaša Apr 07 '17 at 06:50
7

I'd like to share approach that did the job in my case. I've created following directive:

import { Directive } from '@angular/core';
import { ControlContainer, NgForm } from '@angular/forms';

@Directive({
  selector: '[appUseParentForm]',
  providers: [
    {
      provide: ControlContainer,
      useFactory: function (form: NgForm) {
        return form;
      },
      deps: [NgForm]
    }
  ]
})
export class UseParentFormDirective {
}

Now, if you use this directive on child component, for example:

<address app-use-parent-form></address>

controls from AddressComponent will be added to form1. As a result form validity will also depend on state of controls inside child component.

Checked only with Angular 6

Maczaj Srtgth
  • 377
  • 4
  • 6
7

With the latest versions of Angular 2+ you can spit your form in multiple components without passing the formGroup instance to the child as input.

You can achieve this by importing this provider into your child component:

viewProviders: [{provide: ControlContainer,useExisting: FormGroupDirective}]

and then you can access to the main form by injecting the formGroupDirective as well:

constructor(private readonly formGroupDirective: FormGroupDirective)
Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
elia
  • 126
  • 1
  • 5
1

Most upvoted comment is boilerplate: I have seen project with this approach and it's a disaster to manage.

I would like to thank user @elia for their answer.

To make it a bit more convenient I can only add, that we can place provider in constant:

import { Provider } from '@angular/core';
import { ControlContainer, FormGroupDirective } from '@angular/forms';

export const FormPart: Provider = {
  provide: ControlContainer,
  useExisting: FormGroupDirective,
};

and import it in desired component like:

@Component({
  ...
  viewProviders: [FormPart],
})

and also create directive with mentioned constructor code which provide parent form if needed, so we can separate form business logic into smaller chunk.

I have tried to explore how decorators function in Angular to extend @Component with such viewProvider, but it's more complex topic, seems like it's not possible, so I gave up. Will be glad to hear other's experience on this. I think it's a nice feature for Angular to support and make work with complex forms easier.

Maxime Lyakhov
  • 140
  • 1
  • 11