content - Formulario controlado por plantilla anidada Angular2
page title angular 4 (5)
De
docs
oficiales:
This directive can only be used as a child of NgForm.
Por lo tanto, creo que puede intentar envolver su componente secundario en diferentes
ngForm
, y esperar en el resultado del componente primario
@Output
del componente secundario.
Avísame si necesitas más aclaraciones.
ACTUALIZACIÓN: Aquí está Plunker con algunos cambios, convertí el formulario hijo a modelo impulsado, porque no hay forma de escuchar el formulario impulsado por formulario para actualizarlo antes de que se envíe.
Esto es una locura, parece que no hay forma de tener una forma de que una de sus entradas esté en un componente hijo.
He leído todos los blogs y tutoriales y todo, no hay forma de resolver esto.
El problema es cuando un componente hijo tendrá algún tipo de directivas de formulario (ngModel, ngModelGroup o lo que sea ...), no funcionará.
Esto es solo un problema en los formularios basados en plantillas
Este es el 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;
}
}
He creado una solución utilizando una directiva y un servicio. Una vez que los agregue a su módulo, el único otro cambio de código que debe realizar es a nivel de formulario en las plantillas. Esto funciona con campos de formulario agregados dinámicamente y AOT. También admite múltiples formas no relacionadas en una página. Aquí está el plunker: plunker .
Utiliza esta directiva:
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);
}
}
Y este servicio:
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] || [];
}
}
Para usar la directiva en un componente padre con un formulario:
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();
}
Luego en un componente hijo:
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;
}
Leyendo a través de un montón de problemas relacionados con github
[1]
[2]
, no he encontrado una manera sencilla de hacer angular agregar controles de un
Component
hijo a un
ngForm
padre (algunas personas también los llaman formas anidadas, entradas anidadas o complejas controles).
Entonces, lo que voy a mostrar aquí es una solución alternativa que
funciona para mí
, utilizando directivas separadas de
ngForm
para padres e hijos.
No es perfecto, pero me acerca lo suficiente como para detenerme allí.
Declaro mi
childFormComponent
con una directiva
ngForm
(es decir,
no es
una etiqueta de formulario html, solo la directiva):
<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>
...
El Componente luego expone el
addressFieldsForm
como una propiedad, y también se exporta a sí mismo como una
variable de referencia de plantilla
:
@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;
....
El formulario principal puede usar el componente de formulario secundario de esta manera:
<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>
Tenga en cuenta que el botón Enviar comprueba explícitamente el estado válido tanto en el
ngFormAddress
como en el de
addressFields
.
De esa manera, al menos puedo componer formas complejas con sensatez, a pesar de que tiene algunas repeticiones.
Otra posible solución alternativa:
@Directive({
selector: ''[provide-parent-form]'',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}
]
})
export class ProvideParentForm {}
Simplemente coloque esta directiva en un componente hijo en algún lugar en la parte superior de la jerarquía de nodos (antes de cualquier ngModel).
Cómo funciona : NgModel qualifies la búsqueda de dependencia del formulario principal con @Host (). Por lo tanto, un formulario de un componente primario no es visible para NgModel en un componente secundario. Pero podemos inyectarlo y proporcionarlo dentro de un componente secundario utilizando el código que se muestra arriba.
Una solución simple es proporcionar
ControlContainer
en la matriz
viewProviders
de su componente hijo como:
import { ControlContainer, NgForm } from ''@angular/forms'';
@Component({
...,
viewProviders: [ { provide: ControlContainer, useExisting: NgForm } ]
})
export class ChildComponent {}
Lea también este artículo que explica por qué está funcionando.
Actualizar
Si está buscando una forma dirigida por modelo anidado , este es el enfoque similar:
@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))
}
}
Actualización 2
Si no sabe exactamente qué tipo de
ControlContainer
envuelve su componente personalizado (por ejemplo, sus controles están dentro de la directiva FormArray), simplemente use la versión común:
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 {}