76

This is just madness , looks like there is no way to have a form which one of it's inputs is in a child component .

I have read all the blogs and tutorials and everything , no way to work this out .

The problem is when a child component is going to have any kind of form directives ( ngModel , ngModelGroup or whatever ..) , it wont work.

This is only a problem in template driven forms

This is the plunker :

import { Component } from '@angular/core';

@Component({
  selector: 'child-form-component',
  template: ` 
  <fieldset ngModelGroup="address">
    <div>
      <label>Street:</label>
      <input type="text" name="street" ngModel>
    </div>
    <div>
      <label>Zip:</label>
      <input type="text" name="zip" ngModel>
    </div>
    <div>
      <label>City:</label>
      <input type="text" name="city" ngModel>
    </div>
  </fieldset>`
})

export class childFormComponent{


}

@Component({
  selector: 'form-component',
  directives:[childFormComponent],
  template: `
    <form #form="ngForm" (ngSubmit)="submit(form.value)">
      <fieldset ngModelGroup="name">
        <div>
          <label>Firstname:</label>
          <input type="text" name="firstname" ngModel>
        </div>
        <div>
          <label>Lastname:</label>
          <input type="text" name="lastname" ngModel>
        </div>
      </fieldset>

      <child-form-component></child-form-component>

      <button type="submit">Submit</button>
    </form>

    <pre>
      {{form.value | json}}
    </pre>

    <h4>Submitted</h4>
    <pre>    
      {{value | json }}
    </pre>
  `
})
export class FormComponent {

  value: any;

  submit(form) {
    this.value = form; 
  }
}
Jacob Stamm
  • 1,660
  • 1
  • 29
  • 53
  • 3
    See also https://medium.com/@a.yurich.zuev/angular-nested-template-driven-form-4a3de2042475 – yurzui Oct 26 '17 at 09:17

6 Answers6

96

One simple solution is to provide ControlContainer in viewProviders array of your child component like:

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

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

Stackblitz Example

Read also this article that explains why it's working.

Update

If you're looking for nested model driven form then here is the similar approach:

@Component({
  selector: 'my-form-child',
  template: `<input formControlName="age">`,
  viewProviders: [
    {
      provide: ControlContainer,
      useExisting: FormGroupDirective
    }
  ]
})
export class ChildComponent {
  constructor(private parent: FormGroupDirective) {}

  ngOnInit() {
    this.parent.form.addControl('age', new FormControl('', Validators.required))
  }
}

Ng-run Example

Update 2

If you don't know exactly which type of ControlContainer wraps your custom component(for example your controls is inside FormArray directive) then just use common version:

import { SkipSelf } from '@angular/core';
import { ControlContainer} from '@angular/forms';

@Component({
 ...,
 viewProviders: [{
   provide: ControlContainer,
   useFactory: (container: ControlContainer) => container,
   deps: [[new SkipSelf(), ControlContainer]],
 }]
})
export class ChildComponent {}

Ng-run Example

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • 4
    Holy shit. I've been waiting FOREVER for this feature! Thanks for the post. – Chris Smith Oct 26 '17 at 17:26
  • 2
    This isn't working for me - I getting a "No provider for NgForm!". I suspect it's because our custom form control has a where the various controls are then inserted. Any idea what the work-around for this might be? – Jake Shakesworth Dec 01 '17 at 00:35
  • @JakeShakesworth Please create plunker or stackblitz example to reproduce – yurzui Dec 01 '17 at 04:20
  • In the nested template driven form approach, everything is working fine, But how to get the parent form object in child component? – Nimish goel Aug 19 '18 at 06:10
  • 1
    @Nimishgoel `this.parent.form` refers to parent FormGroup in my last example – yurzui Aug 19 '18 at 06:12
  • Is there any way, I can get the child component form controls in the child component as well? I need to show the validation error message in the child component form input fields. – Nimish goel Aug 19 '18 at 06:21
  • 2
    SO simple, just one line... `viewProviders: [{provide: ControlContainer, useExisting: NgForm}]` :) – Mick Oct 27 '18 at 13:52
  • yurzui, coming back after some time on a component using your implementation and getting @JakeShakesworth issue. I'm investigating. – netalex Feb 25 '19 at 22:30
  • nice solution, but how can I test it? now my unit-tests fail! – ms86 Mar 12 '19 at 11:05
  • @yurzui - can you help with two-level hierarchy. for ex. check this https://stackblitz.com/edit/angular-xpv7n3 How can we get zip under address? Thanks! – Manish Jain Jun 10 '19 at 22:42
  • 1
    @ManishJain You need to use NgModelGroup for viewProviders in zip component https://stackblitz.com/edit/angular-qse4xu?file=app/zip.component.ts or more abstract way by providing ControlContainer as I described in Update2 https://stackblitz.com/edit/angular-rbkufw?file=app/zip.component.ts. Also if you don't want to have additional property like `{ zip: { zip: '' }}` then remove `
    ` wrapper from your zip component https://stackblitz.com/edit/angular-ixlr45?file=app/zip.component.ts
    – yurzui Jun 11 '19 at 02:02
  • 1
    You are a rockstar @yurzui, thank you!!! add me to your top fan list ;) – Manish Jain Jun 11 '19 at 16:09
  • This should be the accepted answer. Was stuck on this same issue for a couple hours until I came across this post. This seems to be the only post that addresses this issue too – Allen Rufolo Jun 17 '19 at 20:53
  • 1
    If the component is also used outside forms, then you can add `new Optional()` to the deps array: `deps: [[new Optional(), new SkipSelf(), ControlContainer]]`. – Mihai238 May 06 '20 at 06:52
  • Update 2 solution works great but it breaks updateOn: 'blur' validation, for some reason it still triggers validators on every change in the input. Is that a bug? Is there a solution for that? – fingust Jun 23 '21 at 13:41
  • Is it possible to register a FormGroup on a NgForm like this? I tried it but it looks like the FormGroup in the child really expects another FormGroup in the parent. – spierala Aug 20 '21 at 09:41
22

Reading through a bunch of related github issues [1] [2], I haven't found a straightforward way to make angular add a child Component's controls to a parent ngForm (some people also call them nested forms, nested inputs or complex controls).

So what I'm going to show here is a workaround that works for me, using separate ngForm directives for parents and childs. It's not perfect, but it gets me close enough that I stopped there.

I declare my childFormComponent with an ngForm directive (i.e. it's not a html form tag, only the directive):

<fieldset ngForm="addressFieldsForm" #addressFieldsForm="ngForm">
  <div class="form-group">
    <label for="email">Email</label>
    <input type="email" class="form-control" [(ngModel)]="model.email" name="email" #email="ngModel" required placeholder="Email">
  </div>
  ...

The Component then exposes the addressFieldsForm as a property, and also exports itself as a template reference variable:

@Component({
  selector: 'mst-address-fields',
  templateUrl: './address-fields.component.html',
  styleUrls: ['./address-fields.component.scss'],
  exportAs: 'mstAddressFields'
})
export class AddressFieldsComponent implements OnInit {
  @ViewChild('addressFieldsForm') public form: NgForm;
  ....

The parent form can then use the child form component like this:

  <form (ngSubmit)="saveAddress()" #ngFormAddress="ngForm" action="#">
    <fieldset>
      <mst-address-fields [model]="model" #addressFields="mstAddressFields"></mst-address-fields>
      <div class="form-group form-buttons">
        <button class="btn btn-primary" type="submit" [disabled]="!ngFormAddress.valid || !addressFields.form.valid">Save</button>
      </div>
    </fieldset>
  </form>

Note that the submit button explicitly checks valid state on both the ngFormAddress and the addressFields form. That way I can at least sensibly compose complex forms, even though it has some boilerplate.

Johannes Rudolph
  • 35,298
  • 14
  • 114
  • 172
  • 1
    Johannes Rudolph for president. You saved my day :) Thanks! – speti43 Jun 22 '17 at 12:24
  • 5
    Based on your answer, I found a way so child components can auto-wire themselves into a shared form serice: https://gist.github.com/jehugaleahsa/c40fb64d8613cfad8f1e1faa4c2a7e33 – Travis Parks Jun 24 '17 at 12:45
  • @TravisParks you should add your complete Solution as a new answer on this post its working great! THANK YOU!! – squadwuschel Jun 27 '17 at 20:05
12

Another possible workaround:

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

Just place this directive in a child component somewhere at the top of nodes hierarchy (before any ngModel).

How it works: NgModel qualifies parent form's dependency lookup with @Host(). So a form from a parent component is not visible to NgModel in a child component. But we can inject and provide it inside a child component using the code demonstrated above.

Artem Andreev
  • 19,942
  • 5
  • 43
  • 42
  • Excellent solution! You should write a blog post about this. – S.Klechkovski Jul 27 '17 at 09:48
  • I'd love a blog post to go into depth more on this – CaffGeek Aug 24 '17 at 20:57
  • @artem it's not working in my sample plunker: https://plnkr.co/edit/VSUzQtrtEmXSIEPzAjXb Do you know why? – Martino Bordin Sep 04 '17 at 10:01
  • @MartinoBordin, I'm not sure what is not working for you. I've checked it a few seconds ago - if I clear any child input I'm getting "Valid: false" – Artem Andreev Sep 04 '17 at 12:12
  • @ArtemAndreev yes, but if you type a char into one child input (while the others are still empty and in error) the form would be "Valid:true" – Martino Bordin Sep 04 '17 at 12:14
  • 1
    @MartinoBordin, ok, that's because you have the same name for all child-inputs. You need to have something like ```name="childInput-{{rowNumber}}"``` instead of ```name="childInput"``` – Artem Andreev Sep 04 '17 at 12:25
  • 1
    @ArtemAndreev I tried what you suggested and it is almost working: https://plnkr.co/edit/WzFsoFbWRgIAu8Nv3Lst If you delete for example the first item ('Apple) and then you add another child, the second binding will be wrong – Martino Bordin Sep 04 '17 at 14:47
  • Is important to note that the injected form can be accessed in the child component by declaring the form property in the constructor, by using public form: NgForm. – dovahkiin May 25 '19 at 01:06
2

From official docs: This directive can only be used as a child of NgForm.

So I think you can try to wrap your child component in different ngForm, and expect in parent component result @Output of child component. Let me know if you need more clarification.

UPDATE: Here is Plunker with some changes, I converted child form to model driven, because there is no way to listen on form driven form for updated before it will be submited.

Kanso Code
  • 7,479
  • 5
  • 34
  • 49
  • Components are isolated in ng2, so that should answered for first sentence . Attaching plunker – Kanso Code Aug 31 '16 at 08:42
  • 3
    You're using reactive way of defining the controls , where as the whole question is about template driven forms. –  Aug 31 '16 at 09:56
  • I have answered your question, why it is not working - because your child component have no ngForm. – Kanso Code Aug 31 '16 at 10:14
  • Your answer is related to reactive forms and here is about template driven. any solution? – DAG Jul 04 '17 at 14:56
2

I've created a solution using a directive and service. Once you add those to your module, the only other code change you need to make are at the form level in the templates. This works with dynamically added form fields and AOT. It also supports multiple unrelated forms on a page. Here's the plunker: plunker.

It uses this directive:

import { Directive, Input } from '@angular/core';
import { NgForm } from '@angular/forms';
import { NestedFormService } from './nested-form.service';

@Directive({
    selector: '[nestedForm]',
    exportAs: 'nestedForm'   
})
export class NestedFormDirective {    
    @Input('nestedForm') ngForm: NgForm;
    @Input() nestedGroup: string;
       
    public get valid() {
        return this.formService.isValid(this.nestedGroup);
    }

    public get dirty() {
        return this.formService.isDirty(this.nestedGroup);
    }

    public get touched() {
        return this.formService.isTouched(this.nestedGroup);
    }
    
    constructor(      
        private formService: NestedFormService
    ) { 
        
    }

    ngOnInit() {   
        this.formService.register(this.ngForm, this.nestedGroup);
    }

    ngOnDestroy() {
        this.formService.unregister(this.ngForm, this.nestedGroup);
    } 

    reset() {
        this.formService.reset(this.nestedGroup);
    }
}

And this service:

import { Injectable } from '@angular/core';
import { NgForm } from '@angular/forms';

@Injectable()
export class NestedFormService {

    _groups: { [key: string] : NgForm[] } = {};
      
    register(form: NgForm, group: string = null) {           
        if (form) {
            group = this._getGroupName(group);
            let forms = this._getGroup(group);        
            if (forms.indexOf(form) === -1) {
                forms.push(form);
                this._groups[group] = forms;
            }
        }
    }

    unregister(form: NgForm, group: string = null) {        
        if (form) {
            group = this._getGroupName(group);
            let forms = this._getGroup(group);
            let i = forms.indexOf(form);
            if (i > -1) {
                forms.splice(i, 1);
                this._groups[group] = forms;
            }
        }
    }

    isValid(group: string = null) : boolean {   
        group = this._getGroupName(group);         
        let forms = this._getGroup(group);
       
        for(let i = 0; i < forms.length; i++) {
            if (forms[i].invalid)
                return false;
        }
        return true;
    } 

    isDirty(group: string = null) : boolean {   
        group = this._getGroupName(group);         
        let forms = this._getGroup(group);
       
        for(let i = 0; i < forms.length; i++) {
            if (forms[i].dirty)
                return true;
        }
        return false;
    } 

    isTouched(group: string = null) : boolean {   
        group = this._getGroupName(group);         
        let forms = this._getGroup(group);
       
        for(let i = 0; i < forms.length; i++) {
            if (forms[i].touched)
                return true;
        }
        return false;
    } 

    reset(group: string = null) {
        group = this._getGroupName(group);         
        let forms = this._getGroup(group);
       
        for(let i = 0; i < forms.length; i++) {
            forms[i].onReset();
        }
    }

    _getGroupName(name: string) : string {
        return name || '_default';
    }

    _getGroup(name: string) : NgForm[] {        
        return this._groups[name] || [];
    }          
}

To use the directive in a parent component with a form:

import { Component, Input } from '@angular/core';
import { Person } from './person.model';

@Component({
    selector: 'parent-form',
    template: `  
        <div class="parent-box">

            <!--
            ngForm                        Declare Angular Form directive
            #theForm="ngForm"             Assign the Angular form to a variable that can be used in the template
            [nestedForm]="theForm"        Declare the NestedForm directive and pass in the Angular form variable as an argument
            #myForm="nestedForm"          Assign the NestedForm directive to a variable that can be used in the template
            [nestedGroup]="model.group"   Pass a group name to the NestedForm directive so you can have multiple forms on the same page (optional).
            -->

            <form 
                ngForm                  
                #theForm="ngForm" 
                [nestedForm]="theForm"
                #myForm="nestedForm" 
                [nestedGroup]="model.group">        

                <h3>Parent Component</h3> 
                <div class="pad-bottom">
                    <span *ngIf="myForm.valid" class="label label-success">Valid</span>
                    <span *ngIf="!myForm.valid" class="label label-danger">Not Valid</span>
                    <span *ngIf="myForm.dirty" class="label label-warning">Dirty</span>    
                    <span *ngIf="myForm.touched" class="label label-info">Touched</span>    
                </div> 

                <div class="form-group" [class.hasError]="firstName.invalid">
                    <label>First Name</label>
                    <input type="text" id="firstName" name="firstName" [(ngModel)]="model.firstName" #firstName="ngModel" class="form-control" required />
                </div>

                <child-form [model]="model"></child-form>
               
                <div>
                    <button type="button" class="btn btn-default" (click)="myForm.reset()">Reset</button>
                </div>
            </form>   
        </div>
    `
})
export class ParentForm {   
    
    model = new Person();
   
}

Then in a child component:

import { Component, Input } from '@angular/core';
import { Person } from './person.model';

@Component({
    selector: 'child-form',
    template: `  
        <div ngForm #theForm="ngForm" [nestedForm]="theForm" [nestedGroup]="model.group" class="child-box">
            <h3>Child Component</h3>
            <div class="form-group" [class.hasError]="lastName.invalid">
                <label>Last Name</label>
                <input type="text" id="lastName" name="lastName" [(ngModel)]="model.lastName" #lastName="ngModel" class="form-control" required />
            </div>
        </div>  
    `
})
export class ChildForm {    
    @Input() model: Person;
      
}
nuckolhead
  • 91
  • 1
  • 4
1

With ~100 controls in dynamic forms, the implicit inclusion of controls may make you a template-driven juggernaut. The following will apply yurzui's miracle everywhere.

export const containerFactory = (container: ControlContainer) => container;

export const controlContainerProvider = [{
  provide: ControlContainer,
  deps: [[new Optional(), new SkipSelf(), ControlContainer]],
  useFactory: containerFactory
}]

@Directive({
  selector: '[ngModel]',
  providers: [controlContainerProvider]
})
export class ControlContainerDirective { }

Provide controlContainerProvider to components with NgModelGroup.

StackBlitz Example

Forms require controls to set a name attribute by default. Use the following directive to remove this requirement, and include controls only when a name attribute is set.

import { Directive, ElementRef, HostBinding, OnInit } from '@angular/core';
import { ControlContainer, NgModel } from '@angular/forms';

@Directive({
  selector: '[ngModel]:not([name]):not([ngModelOptions])',
  providers: [{
    provide: ControlContainer,
    useValue: null
  }]
})
export class StandaloneDirective implements OnInit { }

StackBlitz Example

Trevor Karjanis
  • 1,485
  • 14
  • 25