<!DOCTYPE html>
<html>
<head>
<base href="." />
<title>angular2 playground</title>
<link rel="stylesheet" href="style.css" />
<script src="https://unpkg.com/core-js@2.4.1/client/shim.min.js"></script>
<script src="https://unpkg.com/zone.js/dist/zone.js"></script>
<script src="https://unpkg.com/zone.js/dist/long-stack-trace-zone.js"></script>
<script src="https://unpkg.com/reflect-metadata@0.1.3/Reflect.js"></script>
<script src="https://unpkg.com/systemjs@0.19.31/dist/system.js"></script>
<script src="config.js"></script>
<script>
System.import('app')
.catch(console.error.bind(console));
</script>
</head>
<body>
<app-nested-form>
loading...
</app-nested-form>
</body>
</html>
/* Styles go here */
### Angular Starter Plunker - Typescript
System.config({
//use typescript for compilation
transpiler: 'typescript',
//typescript compiler options
typescriptOptions: {
emitDecoratorMetadata: true
},
paths: {
'npm:': 'https://unpkg.com/'
},
//map tells the System loader where to look for things
map: {
'app': './src',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
'lodash': 'npm:lodash/lodash.js',
'rxjs': 'npm:rxjs',
'typescript': 'npm:typescript@2.0.2/lib/typescript.js'
},
//packages defines our app package
packages: {
app: {
main: './main.ts',
defaultExtension: 'ts'
},
rxjs: {
defaultExtension: 'js'
}
}
});
//main entry point
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {NestedFormsModule} from './app';
platformBrowserDynamic().bootstrapModule(NestedFormsModule)
import {
BrowserModule
} from '@angular/platform-browser';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Input,
OnInit,
NgModule
} from '@angular/core';
import {
CommonModule
} from '@angular/common';
import {
FormArray,
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import * as _ from 'lodash';
export interface ParentData {
parentField1: string;
parentField2: string;
parentHiddenField1: string;
children: ChildData[];
};
export interface ChildData {
id: number;
childField1: string;
childField2: string;
childHiddenField1: string;
kid: InfantData[];
};
export interface InfantData {
id: number;
infantField: string;
};
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-nested-form',
templateUrl: './nested-form.component.html'
})
export class NestedFormComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
@Component({
selector: 'app-parent-form',
templateUrl: './parent-form.component.html',
styleUrls: [ './parent-form.component.css' ]
})
export class ParentFormComponent implements OnInit, AfterViewInit {
public initialState: ParentData;
public parentData: ParentData;
public parentForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.initialState = this.getParentData();
this.parentData = _.cloneDeep(this.initialState);
this.parentForm = this.toFormGroup(this.parentData);
console.log('Initial parentData', this.parentData);
}
ngAfterViewInit() {
this.parentForm.valueChanges
.subscribe(value => {
console.log('Parent Form changed', value);
this.parentData = _.mergeWith(this.parentData,
value,
this.mergeCustomizer);
});
}
private getParentData(): ParentData {
return {
parentField1: 'Parent Field 1 Value',
parentField2: 'Parent Field 2 Value',
parentHiddenField1: 'Parent Hidden Field 1 Value',
children: [{
id: 1,
childField1: 'Child 1 - Child Field 1 Value',
childField2: 'Child 1 - Child Field 2 Value',
childHiddenField1: 'Child 1 - Child Hidden Field 1 Value',
kid: [{
id:Math.floor(Math.random() * 100) ,
infantField: 'test1'
}]
}, {
id: 2,
childField1: 'Child 2 - Child Field 1 Value',
childField2: 'Child 2 - Child Field 2 Value',
childHiddenField1: 'Child 2 - Child Hidden Field 1 Value',
kid: [{
id:Math.floor(Math.random() * 100) ,
infantField: 'test2'
}]
}]
};
}
private toFormGroup(data: ParentData): FormGroup {
const formGroup = this.fb.group({
parentField1: [ data.parentField1, Validators.required ],
parentField2: [ data.parentField2, Validators.required ],
parentHiddenField1: [ data.parentHiddenField1 ]
});
return formGroup;
}
// _.mergeWith customizer to avoid merging primitive arrays, and only
// merge object arrays
private mergeCustomizer = (objValue, srcValue) => {
if (_.isArray(objValue)) {
if (_.isPlainObject(objValue[0]) || _.isPlainObject(srcValue[0])) {
return srcValue.map(src => {
const obj = _.find(objValue, { id: src.id });
return _.mergeWith(obj || {}, src, this.mergeCustomizer);
});
}
return srcValue;
}
}
onSubmit() {
if (!this.parentForm.valid) {
console.error('Parent Form invalid, preventing submission');
return false;
}
const updatedParentData = _.mergeWith(this.parentData,
this.parentForm.value,
this.mergeCustomizer);
console.log('Submitting...');
console.log('Original parentData', this.initialState);
console.log('Updated parentData', updatedParentData);
return false;
}
}
@Component({
selector: 'app-child-list',
templateUrl: './child-list.component.html',
styleUrls: [ './child-list.component.css' ]
})
export class ChildListComponent implements OnInit {
@Input('parentForm')
public parentForm: FormGroup;
@Input('children')
public children: ChildData[];
constructor(private cd: ChangeDetectorRef) { }
ngOnInit() {
console.log('Initializing child list', this.children);
this.parentForm.addControl('children', new FormArray([]));
}
addChild() {
const child: ChildData = {
id: Math.floor(Math.random() * 100),
childField1: '',
childField2: '',
childHiddenField1: '',
kid: []
};
this.children.push(child);
this.cd.detectChanges();
return false;
}
removeChild(idx: number) {
if (this.children.length > 1) {
this.children.splice(idx, 1);
(<FormArray>this.parentForm.get('children')).removeAt(idx);
}
return false;
}
}
@Component({
selector: 'app-child-form',
templateUrl: './child-form.component.html',
styleUrls: [ './child-form.component.css' ]
})
export class ChildFormComponent implements OnInit {
@Input('children')
public children: FormArray;
@Input('child')
public child: ChildData;
public childForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
console.log('Initializing child form', this.child);
this.childForm = this.toFormGroup(this.child);
this.children.push(this.childForm);
}
private toFormGroup(data: ChildData) {
const formGroup = this.fb.group({
id: [ data.id ],
childField1: [ data.childField1 || '', Validators.required ],
childField2: [ data.childField2 || '', Validators.required ],
childHiddenField1: [ data.childHiddenField1 ]
});
return formGroup;
}
}
/* kid -from*/
@Component({
selector: 'app-kid-form',
templateUrl: './kid-form.component.html',
styleUrls: [ './kid-form.component.css' ]
})
export class KidFormComponent implements OnInit {
@Input('kid')
public kid: FormArray;
@Input('infant')
public infant: InfantData;
@Input('infantForm')
public infantForm: FormGroup;
public initialState: InfantData;
constructor(
private fb: FormBuilder
) {}
ngOnInit() {
console.log('Initializing kid form', this.infant);
this.infantForm = this.toFormGroup(this.infant);
this.kid.push(this.infantForm);
}
private toFormGroup(data: InfantData) {
const formGroup = this.fb.group({
id: [ data.id ],
infantField: [ data.infantField || '', Validators.required ]
});
return formGroup;
}
}
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { FormArray, FormGroup } from '@angular/forms';
@Component({
selector: 'app-kid-list',
templateUrl: './kid-list.component.html',
styleUrls: [ './kid-list.component.css' ]
})
export class KidListComponent implements OnInit {
@Input('childForm')
public childForm: FormGroup;
@Input('kid')
public kid: InfantData[];
public initialState: InfantData;
constructor(private cd: ChangeDetectorRef) { }
ngOnInit() {
console.log('Initializing kid list', this.kid);
this.childForm.addControl('kid', new FormArray([]));
}
addInfant() {
const infant: InfantData = {
id: Math.floor(Math.random() * 100),
infantField: ''
};
this.kid.push(infant);
this.cd.detectChanges();
return false;
}
removeInfant(idx: number) {
if (this.kid.length > 1) {
this.kid.splice(idx, 1);
(<FormArray>this.childForm.get('kid')).removeAt(idx);
}
return false;
}
}
@NgModule({
imports: [
BrowserModule,
CommonModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
NestedFormComponent,
ParentFormComponent,
ChildFormComponent,
ChildListComponent,
KidFormComponent,
KidListComponent
],
bootstrap: [
NestedFormComponent
]
})
export class NestedFormsModule { }
<h1>Nested Forms Example</h1>
<app-parent-form></app-parent-form>
<h2>
Parent Form
</h2>
<form class="parent-form"
[formGroup]="parentForm"
(ngSubmit)="onSubmit()">
<div>
<label for="parentField1">Parent Field 1</label>
<input formControlName="parentField1" />
</div>
<div>
<label for="parentField2">Parent Field 2</label>
<input formControlName="parentField2" />
</div>
<app-child-list
[parentForm]="parentForm"
[children]="parentData.children">
</app-child-list>
<button type="submit" [disabled]="!parentForm.valid">
Submit
</button>
</form>
<div class="parent-form-state">
<p>Current parentForm State:</p>
<pre>{{ parentData | json }}</pre>
</div>
<h3>
Children
</h3>
<div class="child-list"
[formGroup]="parentForm">
<div formArrayName="children">
<div *ngFor="let child of children; let idx=index ">
Child {{child.id}}
(<a href="" (click)="removeChild(idx)">Remove</a>)
<app-child-form
[children]="parentForm.controls.children"
[child]="child">
</app-child-form>
</div>
<a href="" (click)="addChild()">
Add Child
</a>
</div>
</div>
<div class="child-form"
[formGroup]="childForm">
<div>
<label for="childField1">Child Field 1</label>
<input formControlName="childField1" />
</div>
<div>
<label for="childField1">Child Field 2</label>
<input formControlName="childField2" />
</div>
<app-kid-list
[childForm]="childForm"
[kid]="child.kid"
>
</app-kid-list>
</div>
.child-form {
border: 1px solid black;
margin-bottom: 20px;
padding: 20px;
}
input {
width: 30%;
min-width: 200px;
}
.child-list {
margin-bottom: 20px;
}
.parent-form {
border: 1px solid black;
margin-bottom: 20px;
padding: 20px;
}
.parent-form-state {
padding: 10px;
}
input {
width: 30%;
min-width: 200px;
}
<div class="infant-form"
[formGroup]="infantForm">
<div>
<label for="infantField">infant Field1</label>
<input formControlName="infantField" />
</div>
</div>
.kid-list {
margin-bottom: 20px;
}
<h3>
Kid
</h3>
<div class="kid-list"
[formGroup]="childForm">
<div formArrayName="kid">
<div *ngFor="let infant of kid; let idx=index ">
Infant {{infant.id}}
(<a href="" (click)="removeInfant(idx)">Remove</a>)
<app-kid-form
[kid]="childForm.controls.kid"
[infant]="infant">
</app-kid-form>
</div>
<button (click)="addInfant()">
Add Infant
</button>
</div>
</div>
.infant-form {
border: 1px solid black;
margin-bottom: 20px;
padding: 20px;
}
input {
width: 30%;
min-width: 200px;
}