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 {}
Прочитайте также эту статью, которая объясняет, почему это работает.
Обновить
Если вы ищете вложенную модель, управляемую формой, то вот похожий подход:
@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))
}
}
Обновление 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 {}
Читая кучу связанных проблем 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.
Формам требуются элементы управления для установки атрибута имени по умолчанию. Используйте следующую директиву, чтобы удалить это требование, и включать элементы управления, только если установлен атрибут 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 { }