Angular2 вложенная шаблонно-управляемая форма

Это просто безумие, похоже, что нет способа получить форму, один из входных элементов которой находится в дочернем компоненте.

Я прочитал все блоги и учебные пособия и все такое, нет способа решить это.

Проблема в том, что когда дочерний компонент будет иметь какие-либо директивы формы (ngModel, ngModelGroup или что-то еще..), он не будет работать.

Это только проблема в шаблонах управляемых форм

Это плункер:

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; 
  }
}

5 ответов

Одним из простых решений является предоставление ControlContainer в viewProviders массив вашего дочернего компонента, например:

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

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

Пример Stackblitz

Прочитайте также эту статью, которая объясняет, почему это работает.

Обновить

Если вы ищете вложенную модель, управляемую формой, то вот похожий подход:

@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 Пример

Обновление 2

Если вы не знаете точно, какой тип ControlContainer оборачивает ваш пользовательский компонент (например, ваши элементы управления находятся внутри директивы FormArray), а затем просто используйте общую версию:

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 Пример

Читая кучу связанных проблем github [1] [2], я не нашел простого способа заставить углового добавить ребенка Componentконтролирует родителя ngForm (некоторые люди также называют их вложенными формами, вложенными входами или сложными элементами управления).

Так что я собираюсь показать здесь, это обходной путь, который работает для меня, используя отдельные ngForm директивы для родителей и детей. Он не идеален, но он достаточно близко подходит для того, чтобы я там остановился.

Я заявляю childFormComponent с ngForm директива (т. е. это не HTML-тег формы, только директива):

<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>
  ...

Компонент затем выставляет addressFieldsForm как свойство, а также экспортирует себя в качестве ссылочной переменной шаблона:

@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;
  ....

Затем родительская форма может использовать дочерний компонент формы следующим образом:

  <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>

Обратите внимание, что кнопка отправки явно проверяет действительное состояние обоих ngFormAddress и addressFields форма. Таким образом, я могу, по крайней мере, разумно составлять сложные формы, даже если у них есть образец.

Другой возможный обходной путь:

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

Просто поместите эту директиву в дочерний компонент где-нибудь наверху иерархии узлов (перед любой ngModel).

Как это работает: NgModel квалифицирует поиск зависимостей родительской формы с помощью @Host(). Таким образом, форма из родительского компонента не видна NgModel в дочернем компоненте. Но мы можем внедрить и предоставить его внутри дочернего компонента, используя код, показанный выше.

Я создал решение, используя директиву и сервис. После того, как вы добавите их в свой модуль, единственное другое изменение кода, которое вам нужно сделать, это на уровне формы в шаблонах. Это работает с динамически добавленными полями формы и AOT. Он также поддерживает несколько не связанных форм на странице. Вот плункер: плункер.

Он использует эту директиву:

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);
    }
}

И этот сервис:

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] || [];
    }          
}

Чтобы использовать директиву в родительском компоненте с формой:

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();
   
}

Затем в дочернем компоненте:

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;
      
}

Из официальных документов: This directive can only be used as a child of NgForm.

Поэтому я думаю, что вы можете попытаться обернуть свой дочерний компонент в разные ngForm и ожидаем в результате родительского компонента @Output дочернего компонента. Дайте мне знать, если вам нужно больше разъяснений.

ОБНОВЛЕНИЕ: Здесь есть Plunker с некоторыми изменениями, я преобразовал дочернюю форму в управляемую моделью, потому что нет возможности прослушивать обновленную форму, управляемую формой, прежде чем она будет отправлена.

При ~100 элементах управления в динамических формах неявное включение элементов управления может сделать вас управляемым шаблоном джаггернаутом. Следующее применит чудо юрзуя везде.

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 { }

Предоставьте controlContainerProvider компонентам с помощью NgModelGroup.

Пример StackBlitz

Формам требуются элементы управления для установки атрибута имени по умолчанию. Используйте следующую директиву, чтобы удалить это требование, и включать элементы управления, только если установлен атрибут name.

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

Другие вопросы по тегам