Skip to content

Commit

Permalink
fix(dynamic-forms): dynamically add, edit, remove Form Controls durin…
Browse files Browse the repository at this point in the history
…g runtime

Refactored form controls to use FormArray API to add, edit, and remove controls.

closes #624
  • Loading branch information
jo186026 committed Jul 25, 2017
1 parent 7f2815d commit aba0a8b
Show file tree
Hide file tree
Showing 8 changed files with 514 additions and 60 deletions.
16 changes: 14 additions & 2 deletions src/platform/dynamic-forms/dynamic-element.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, Directive, Input, HostBinding, OnInit } from '@angular/core';
import { ViewChild, ViewContainerRef } from '@angular/core';
import { ComponentFactoryResolver, ComponentRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormGroup } from '@angular/forms';

import { TdDynamicElement, TdDynamicType, TdDynamicFormsService } from './services/dynamic-forms.service';
import { AbstractControlValueAccessor } from './dynamic-elements/abstract-control-value-accesor';
Expand Down Expand Up @@ -42,7 +42,12 @@ export class TdDynamicElementComponent extends AbstractControlValueAccessor
/**
* Sets form control of the element.
*/
@Input() dynamicControl: FormControl;
@Input() dynamicControl: FormGroup;

/**
* Maps control object in FormGroup.
*/
@Input() elementName: string;

/**
* Sets label to be displayed.
Expand Down Expand Up @@ -106,6 +111,7 @@ export class TdDynamicElementComponent extends AbstractControlValueAccessor
.create(this.childElement.viewContainer.injector);
this.childElement.viewContainer.insert(ref.hostView);
ref.instance.control = this.dynamicControl;
ref.instance.elementName = this.elementName;
ref.instance.label = this.label;
ref.instance.type = this.type;
ref.instance._value = this._value;
Expand All @@ -115,12 +121,18 @@ export class TdDynamicElementComponent extends AbstractControlValueAccessor
ref.instance.selections = this.selections;
ref.instance.registerOnChange((value: any) => {
this.value = value;

let valueObect: any = {};
valueObect[ this.elementName ] = value;

this.dynamicControl.setValue(valueObect);
});
this.registerOnModelChange((value: any) => {
// fix to check if value is NaN (type=number)
if (!Number.isNaN(value)) {
ref.instance.value = value;
}

});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
flex>
<md-hint align="start">
<span class="tc-red-600">
<span *ngIf="control.hasError('max')">Max: {{max}}</span>
<span *ngIf="control.hasError('min')">Min: {{min}}</span>
<span *ngIf="control.controls[elementName].hasError('max')">Max: {{max}}</span>
<span *ngIf="control.controls[elementName].hasError('min')">Min: {{min}}</span>
</span>
</md-hint>
</md-input-container>
</div>

<div>

</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export class TdDynamicInputComponent extends AbstractControlValueAccessor implem

label: string = '';

elementName: string = '';

type: string = undefined;

required: boolean = undefined;
Expand Down
32 changes: 17 additions & 15 deletions src/platform/dynamic-forms/dynamic-forms.component.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
<form [formGroup]="dynamicForm" novalidate>
<div layout="row"
layout-wrap
layout-margin
layout-align="start center">
<ng-template let-element ngFor [ngForOf]="elements">
<td-dynamic-element [formControlName]="element.name"
[dynamicControl]="dynamicForm.controls[element.name]"
[id]="element.name"
[label]="element.label || element.name"
[type]="element.type"
[required]="element.required"
[min]="element.min"
[max]="element.max"
[selections]="element.selections">
<div layout="row"
layout-wrap
layout-margin
layout-align="start center"
formArrayName="formArray">
<ng-template let-control ngFor [ngForOf]="dynamicForm.controls['formArray'].controls" let-i="index">
<td-dynamic-element
[dynamicControl]="control"
[elementName]="control.tdElementConfig.name"
[id]="control.tdElementConfig.name"
[label]="control.tdElementConfig.label || control.tdElementConfig.name"
[type]="control.tdElementConfig.type"
[required]="control.tdElementConfig.required"
[min]="control.tdElementConfig.min"
[max]="control.tdElementConfig.max"
[selections]="control.tdElementConfig.selections">
</td-dynamic-element>
</ng-template>
</div>
<ng-content></ng-content>
</form>
</form>
179 changes: 140 additions & 39 deletions src/platform/dynamic-forms/dynamic-forms.component.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,33 @@
import { Component, Input, ChangeDetectorRef } from '@angular/core';
import { FormGroup, FormBuilder, AbstractControl } from '@angular/forms';
import { Component, Input, ChangeDetectorRef, DoCheck } from '@angular/core';
import { FormGroup, FormBuilder, AbstractControl, FormArray } from '@angular/forms';

import { TdDynamicFormsService, ITdDynamicElementConfig } from './services/dynamic-forms.service';
import { TdDynamicFormsService, ITdDynamicElementConfig, ITdFormGroup } from './services/dynamic-forms.service';
import { FormArrayUtils } from './services/form-array-utils';
import has = Reflect.has;

export enum constant {
FORM_ARRAY = <any>'formArray',
}

@Component({
selector: 'td-dynamic-forms',
templateUrl: './dynamic-forms.component.html',
})
export class TdDynamicFormsComponent {
export class TdDynamicFormsComponent implements DoCheck {

private _elements: ITdDynamicElementConfig[];
// dirty check
private _elementsLength: number = 0;
private _formArrayUtils: FormArrayUtils;

dynamicForm: FormGroup;
elementConfigs: { [key: string ]: ITdDynamicElementConfig } = {};

/**
* elements: ITdDynamicElementConfig[]
* JS Object that will render the elements depending on its config.
* [name] property is required.
*/
@Input('elements')
set elements(elements: ITdDynamicElementConfig[]){
if (elements) {
if (this._elements) {
this._elements.forEach((elem: ITdDynamicElementConfig) => {
this.dynamicForm.removeControl(elem.name);
});
}
let duplicates: string[] = [];
elements.forEach((elem: ITdDynamicElementConfig) => {
this._dynamicFormsService.validateDynamicElementName(elem.name);
if (duplicates.indexOf(elem.name) > -1) {
throw new Error(`Dynamic element name: "${elem.name}" is duplicated`);
}
duplicates.push(elem.name);
this.dynamicForm.registerControl(elem.name, this._dynamicFormsService.createFormControl(elem));
});
this._elements = elements;
this._changeDetectorRef.detectChanges();
}
}
get elements(): ITdDynamicElementConfig[] {
return this._elements;
}
@Input('elements') elements: ITdDynamicElementConfig[] = [];

/**
* Getter property for dynamic [FormGroup].
Expand All @@ -65,38 +51,153 @@ export class TdDynamicFormsComponent {
*/
get value(): any {
if (this.dynamicForm) {
return this.dynamicForm.value;
return this._formArrayUtils.getValue();
}
return {};
}

/**
* Getter property for [errors] of dynamic [FormGroup].
*/
get errors(): {[name: string]: any} {
get errors(): { [name: string]: any } {
if (this.dynamicForm) {
let errors: {[name: string]: any} = {};
for (let name in this.dynamicForm.controls) {
errors[name] = this.dynamicForm.controls[name].errors;
}
return errors;
return this._formArrayUtils.getErrors();
}
return {};
}

/**
* Getter property for [controls] of dynamic [FormGroup].
*/
get controls(): {[key: string]: AbstractControl} {
get controls(): { [key: string]: AbstractControl } {
if (this.dynamicForm) {
return this.dynamicForm.controls;
return this._formArrayUtils.getControls();
}
return {};
}

constructor(private _formBuilder: FormBuilder,
private _dynamicFormsService: TdDynamicFormsService,
private _changeDetectorRef: ChangeDetectorRef) {
this.dynamicForm = this._formBuilder.group({});
this.dynamicForm = this._formBuilder.group({
formArray: this._formBuilder.array([]),
});

// Not use constructor because FormArray should not be an instance variable.
this._formArrayUtils = new FormArrayUtils();
this._formArrayUtils.formArray = <FormArray>this.dynamicForm.controls[ constant.FORM_ARRAY ];
}

ngDoCheck(): void {

if (!this.elements) {
return;
}

// manual dirty check
if (this.elements.length === this._elementsLength) {
this.compareControls();
} else {
let doAdd: boolean = this.elements.length > this._elementsLength;

// set pristine
this._elementsLength = this.elements.length;

if (doAdd) {
this.pushControls();
} else {
this.removeControls();
}
}

}

pushControls(): void {

let duplicates: string[] = [];
this.elements.forEach((elem: ITdDynamicElementConfig) => {

if (duplicates.indexOf(elem.name) > -1) {
throw new Error(`Dynamic element name: "${elem.name}" is duplicated`);
}
duplicates.push(elem.name);

if (!this._formArrayUtils.contains(elem.name)) {

// tslint:disable-next-line
this.elementConfigs[ elem.name ] = elem;

// Let FormGroup API create the control
// tslint:disable-next-line
(<FormArray>this.dynamicForm.controls[ constant.FORM_ARRAY ]).push(this.createFormGroup(elem));
}
});

this._changeDetectorRef.detectChanges();

}

createFormGroup(elem: ITdDynamicElementConfig): FormGroup {
let group: any = {};
elem.default = elem.default || '';

// group[ elem.name ] = [ defaultValue, validators ];
group[ elem.name ] = this._dynamicFormsService.createFormControl(elem);

let newGroup: FormGroup = this._formBuilder.group(group);
(<ITdFormGroup>newGroup).tdElementConfig = elem;

return newGroup;
}

removeControls(): void {
// tslint:disable-next-line
let arrayControl: FormArray = <FormArray>this.dynamicForm.controls[ constant.FORM_ARRAY ];

let removeControlsByName: string[] = this._formArrayUtils.getControlNamesToRemoveByElements(this.elements);

removeControlsByName.forEach((name: string) => {

// remove ITdDynamicElementConfig reference
delete this.elementConfigs[ name ];

// Need to get index every iteration
// since remove control re-indexes array.
let indexInFormArray: number = this._formArrayUtils.indexOf(name);
arrayControl.removeAt(indexInFormArray);
});

this._changeDetectorRef.detectChanges();
}

compareControls(): void {

let hasChanged: any = [];

this.elements.forEach((elem: ITdDynamicElementConfig, index: number) => {
if (elem !== this.elementConfigs[ elem.name ]) {
hasChanged.push({
index: index,
elem: elem,
});

this.elementConfigs[ elem.name ] = elem;
}
});

if (hasChanged.length) {
while (hasChanged.length) {
let changedElem: any = hasChanged.shift();

// remove old FormGroup
(<FormArray>this.dynamicForm.controls[ constant.FORM_ARRAY ]).removeAt(changedElem.index);

// create new FormGroup
let newGroup: FormGroup = this.createFormGroup(changedElem.elem);

// insert new FormGroup
(<FormArray>this.dynamicForm.controls[ constant.FORM_ARRAY ]).insert(changedElem.index, newGroup);
}
}
}
}
6 changes: 5 additions & 1 deletion src/platform/dynamic-forms/services/dynamic-forms.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable, Provider, SkipSelf, Optional } from '@angular/core';
import { Validators, ValidatorFn, FormControl } from '@angular/forms';
import { Validators, ValidatorFn, FormControl, FormGroup } from '@angular/forms';

import { TdDynamicInputComponent } from '../dynamic-elements/dynamic-input/dynamic-input.component';
import { TdDynamicTextareaComponent } from '../dynamic-elements/dynamic-textarea/dynamic-textarea.component';
Expand Down Expand Up @@ -36,6 +36,10 @@ export interface ITdDynamicElementConfig {
default?: any;
}

export interface ITdFormGroup extends FormGroup {
tdElementConfig: ITdDynamicElementConfig;
}

export const DYNAMIC_ELEMENT_NAME_REGEX: RegExp = /^[a-zA-Z]+[a-zA-Z0-9-_]*$/;

@Injectable()
Expand Down
Loading

0 comments on commit aba0a8b

Please sign in to comment.