From e7be2a8b506de83cf63420f0842ba4f5170d9446 Mon Sep 17 00:00:00 2001 From: Jerry Orta Date: Wed, 9 Aug 2017 18:33:12 -0500 Subject: [PATCH] fix(dynamic-forms): dynamically add, edit, remove Form Controls (#787) * fix(dynamic-forms): #624 dynamically add, edit, remove Form Controls At runtime, have the ability to dynamically add, edit, remove Form Controls closes #624 * fix(dynamic-forms): #624 add validateDynamicElementName back * chore(): simplify code by adding/removing controls and forcing a manual update manual updates are great since checking if an array or its content has changes can be expensive in the long run, and most of the time not needed.. adding a refresh method gives the power to the developer to rerender the form if needed. * chore(dynamic-forms): load README.md and add refresh method to its API * feat(dynamic-forms): make flex width % configurable via element interface * feat(dynamic-forms): support OnPush change detection * feat(dynamic-forms): add demo to build your own form * fix(dynamic-forms): add small fix for times where the control is created a bit later * feat(dynamic-forms): add valid submit button to demo * chore(dynamic-forms): add comments to code * docs(dynamic-forms): new side-by-side pattern --- .../dynamic-forms.component.html | 203 ++++++++++-------- .../dynamic-forms/dynamic-forms.component.ts | 52 +++++ src/platform/dynamic-forms/README.md | 7 +- .../dynamic-element.component.ts | 50 +++-- .../dynamic-input.component.html | 2 +- .../dynamic-forms.component.html | 31 +-- .../dynamic-forms/dynamic-forms.component.ts | 79 +++++-- .../services/dynamic-forms.service.ts | 1 + 8 files changed, 283 insertions(+), 142 deletions(-) diff --git a/src/app/components/components/dynamic-forms/dynamic-forms.component.html b/src/app/components/components/dynamic-forms/dynamic-forms.component.html index 29011a2d0b..e34816ffa4 100644 --- a/src/app/components/components/dynamic-forms/dynamic-forms.component.html +++ b/src/app/components/components/dynamic-forms/dynamic-forms.component.html @@ -2,10 +2,119 @@ Dynamic Forms Build forms from a JS object + +
+ Generated Form + Form Builder +
+
+ +
+
+ + +
+
Toggle to reveal form JSON object
+ +
+
+ +
+ +
+
+ + +
+ + + +
+
+
+ +
+
Select a type, add element then update
+
+
+ +
+
+ + + {{ option }} + + + +
+
+

Form elements

+ + + +
+
+ + + + + + +
+
+ + + + + + +
+
+ Required + + +
+
+
+
+
+ +
+
+
+ +

Dynamic Text Elements

- + Demo @@ -32,7 +141,7 @@

Dynamic Text Elements

Dynamic Number Elements

- + Demo @@ -59,7 +168,7 @@

Dynamic Number Elements

Dynamic Boolean Elements

- + Demo @@ -86,7 +195,7 @@

Dynamic Boolean Elements

Dynamic Array Elements

- + Demo @@ -113,7 +222,7 @@

Dynamic Array Elements

Dynamic File Input Element

- + Demo @@ -136,85 +245,5 @@

Dynamic File Input Element

- - TdDynamicFormsComponent - How to use this component - - -

]]>

-

Use ]]> element to generate a form dynamically.

-

Pass an array of javascript objects that implement [ITdDynamicElementConfig] with the information to be rendered to the [elements] attribute.

- - - -

Properties:

-

The ]]> component has {{dynamicFormsAttrs.length}} properties:

- - - -

{{attr.name}}: {{attr.type}}

-

{{attr.description}}

-
- -
-
-

Example:

-

HTML:

- - - - ]]> - -

Typescript:

- - - -

Setup:

-

Import the [CovalentDynamicFormsModule] in your NgModule:

- - - -
-
\ No newline at end of file + + \ No newline at end of file diff --git a/src/app/components/components/dynamic-forms/dynamic-forms.component.ts b/src/app/components/components/dynamic-forms/dynamic-forms.component.ts index a0db52ad30..d803d23a15 100644 --- a/src/app/components/components/dynamic-forms/dynamic-forms.component.ts +++ b/src/app/components/components/dynamic-forms/dynamic-forms.component.ts @@ -44,11 +44,13 @@ export class DynamicFormsDemoComponent { name: 'input', type: TdDynamicElement.Input, required: false, + flex: 50, }, { name: 'required-input', label: 'Input Label', type: TdDynamicElement.Input, required: true, + flex: 50, }, { name: 'textarea', type: TdDynamicElement.Textarea, @@ -58,6 +60,7 @@ export class DynamicFormsDemoComponent { type: TdDynamicType.Text, required: false, default: 'Default', + flex: 100, }, { name: 'required-password', label: 'Password Label', @@ -110,4 +113,53 @@ export class DynamicFormsDemoComponent { label: 'Browse a file', type: TdDynamicElement.FileInput, }]; + + dynamicElements: ITdDynamicElementConfig[] = [{ + name: 'element-0', + type: TdDynamicType.Text, + required: true, + flex: 80, + }, { + name: 'element-1', + type: TdDynamicType.Number, + required: false, + max: 30, + flex: 20, + }]; + + elementOptions: any[] = [ + TdDynamicElement.Input, + TdDynamicType.Number, + TdDynamicElement.Password, + TdDynamicElement.Textarea, + TdDynamicElement.Slider, + TdDynamicElement.Checkbox, + TdDynamicElement.SlideToggle, + TdDynamicElement.FileInput, + ]; + + showDynamicCode: boolean = false; + + type: any; + + count: number = 2; + + isMinMaxSupported(type: TdDynamicElement | TdDynamicType): boolean { + return type === TdDynamicElement.Slider || type === TdDynamicType.Number; + } + + addElement(): void { + if (this.type) { + this.dynamicElements.push({ + name: 'element-' + this.count++, + type: this.type, + required: false, + }); + this.type = undefined; + } + } + + deleteElement(index: number): void { + this.dynamicElements.splice(index, 1); + } } diff --git a/src/platform/dynamic-forms/README.md b/src/platform/dynamic-forms/README.md index 3a19e3d3d1..04420b3d71 100644 --- a/src/platform/dynamic-forms/README.md +++ b/src/platform/dynamic-forms/README.md @@ -12,6 +12,7 @@ Properties: | `value` | `get(): any` | Getter property for [value] of dynamic [FormGroup]. | `errors` | `get(): {[name: string]: any}` | Getter property for [errors] of dynamic [FormGroup]. | `controls` | `get(): {[key: string]: AbstractControl}` | Getter property for [controls] of dynamic [FormGroup]. +| `refresh` | `function` | Refreshes the form and rerenders all validator/element modifications. ## Setup @@ -54,7 +55,7 @@ Example for HTML usage: ```html - ``` +``` ```typescript import { ITdDynamicElementConfig, TdDynamicElement, TdDynamicType } from '@covalent/dynamic-forms'; @@ -80,6 +81,10 @@ export class Demo { required: true, selections: ['A','B','C'] default: 'A', + }, { + name: 'file-input', + label: 'Label', + type: TdDynamicElement.FileInput, }]; } ``` diff --git a/src/platform/dynamic-forms/dynamic-element.component.ts b/src/platform/dynamic-forms/dynamic-element.component.ts index 0c3e1e2977..582c15e7f9 100644 --- a/src/platform/dynamic-forms/dynamic-element.component.ts +++ b/src/platform/dynamic-forms/dynamic-element.component.ts @@ -1,4 +1,4 @@ -import { Component, Directive, Input, HostBinding, OnInit } from '@angular/core'; +import { Component, Directive, Input, HostBinding, OnInit, SimpleChanges, OnChanges } 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'; @@ -29,7 +29,9 @@ export class TdDynamicElementDirective { template: '
', }) export class TdDynamicElementComponent extends AbstractControlValueAccessor - implements ControlValueAccessor, OnInit { + implements ControlValueAccessor, OnInit, OnChanges { + + private _instance: any; set value(v: any) { if (v !== this._value) { @@ -38,6 +40,9 @@ export class TdDynamicElementComponent extends AbstractControlValueAccessor this.onModelChange(v); } } + get value(): any { + return this._value; + } /** * Sets form control of the element. @@ -77,14 +82,6 @@ export class TdDynamicElementComponent extends AbstractControlValueAccessor @ViewChild(TdDynamicElementDirective) childElement: TdDynamicElementDirective; - @HostBinding('attr.flex') - get flex(): any { - if (this.type) { - return this._dynamicFormsService.getDefaultElementFlex(this.type); - } - return true; - } - @HostBinding('attr.max') get maxAttr(): any { return this.max; @@ -105,25 +102,38 @@ export class TdDynamicElementComponent extends AbstractControlValueAccessor resolveComponentFactory(this._dynamicFormsService.getDynamicElement(this.type)) .create(this.childElement.viewContainer.injector); this.childElement.viewContainer.insert(ref.hostView); - ref.instance.control = this.dynamicControl; - ref.instance.label = this.label; - ref.instance.type = this.type; - ref.instance._value = this._value; - ref.instance.required = this.required; - ref.instance.min = this.min; - ref.instance.max = this.max; - ref.instance.selections = this.selections; - ref.instance.registerOnChange((value: any) => { + this._instance = ref.instance; + this._instance.control = this.dynamicControl; + this._instance.label = this.label; + this._instance.type = this.type; + this._instance.value = this.value; + this._instance.required = this.required; + this._instance.min = this.min; + this._instance.max = this.max; + this._instance.selections = this.selections; + this._instance.registerOnChange((value: any) => { this.value = value; }); this.registerOnModelChange((value: any) => { // fix to check if value is NaN (type=number) if (!Number.isNaN(value)) { - ref.instance.value = value; + this._instance.value = value; } + }); } + /** + * Reassign any inputs that have changed + */ + ngOnChanges(changes: SimpleChanges): void { + if (this._instance) { + for (let prop in changes) { + this._instance[prop] = changes[prop].currentValue; + } + } + } + /** * Implemented as part of ControlValueAccessor. */ diff --git a/src/platform/dynamic-forms/dynamic-elements/dynamic-input/dynamic-input.component.html b/src/platform/dynamic-forms/dynamic-elements/dynamic-input/dynamic-input.component.html index f5d8ab3290..014f2b9c68 100644 --- a/src/platform/dynamic-forms/dynamic-elements/dynamic-input/dynamic-input.component.html +++ b/src/platform/dynamic-forms/dynamic-elements/dynamic-input/dynamic-input.component.html @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/src/platform/dynamic-forms/dynamic-forms.component.html b/src/platform/dynamic-forms/dynamic-forms.component.html index eeaaca7293..d05b1d393b 100644 --- a/src/platform/dynamic-forms/dynamic-forms.component.html +++ b/src/platform/dynamic-forms/dynamic-forms.component.html @@ -1,20 +1,23 @@
-
+
- +
- \ No newline at end of file + diff --git a/src/platform/dynamic-forms/dynamic-forms.component.ts b/src/platform/dynamic-forms/dynamic-forms.component.ts index d096341a28..7a0b5f0c06 100644 --- a/src/platform/dynamic-forms/dynamic-forms.component.ts +++ b/src/platform/dynamic-forms/dynamic-forms.component.ts @@ -1,16 +1,20 @@ -import { Component, Input, ChangeDetectorRef } from '@angular/core'; +import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { FormGroup, FormBuilder, AbstractControl } from '@angular/forms'; import { TdDynamicFormsService, ITdDynamicElementConfig } from './services/dynamic-forms.service'; +import { timer } from 'rxjs/observable/timer'; +import { toPromise } from 'rxjs/operator/toPromise'; + @Component({ selector: 'td-dynamic-forms', templateUrl: './dynamic-forms.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TdDynamicFormsComponent { + private _renderedElements: ITdDynamicElementConfig[] = []; private _elements: ITdDynamicElementConfig[]; - dynamicForm: FormGroup; /** @@ -21,26 +25,14 @@ export class TdDynamicFormsComponent { @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(); + } else { + this._elements = []; } + this._rerenderElements(); } get elements(): ITdDynamicElementConfig[] { - return this._elements; + return this._renderedElements; } /** @@ -73,7 +65,7 @@ export class TdDynamicFormsComponent { /** * 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) { @@ -99,4 +91,53 @@ export class TdDynamicFormsComponent { private _changeDetectorRef: ChangeDetectorRef) { this.dynamicForm = this._formBuilder.group({}); } + + /** + * Refreshes the form and rerenders all validator/element modifications. + */ + refresh(): void { + this._rerenderElements(); + } + + private _rerenderElements(): void { + this._clearRemovedElements(); + this._renderedElements = []; + let duplicates: string[] = []; + this._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); + if (!this.dynamicForm.get(elem.name)) { + this.dynamicForm.addControl(elem.name, this._dynamicFormsService.createFormControl(elem)); + } else { + this.dynamicForm.get(elem.name).setValidators(this._dynamicFormsService.createValidators(elem)); + } + // copy objects so they are only changes when calling this method + this._renderedElements.push(Object.assign({}, elem)); + }); + // call a change detection since the whole form might change + this._changeDetectorRef.detectChanges(); + toPromise.call(timer()).then(() => { + // call a markForCheck so elements are rendered correctly in OnPush + this._changeDetectorRef.markForCheck(); + }); + } + + private _clearRemovedElements(): void { + for (let i: number = 0; i < this._renderedElements.length; i++) { + for (let j: number = 0; j < this._elements.length; j++) { + // check if the name of the element is still there removed + if (this._renderedElements[i].name === this._elements[j].name) { + delete this._renderedElements[i]; + break; + } + } + } + // remove elements that were removed from the array + this._renderedElements.forEach((elem: ITdDynamicElementConfig) => { + this.dynamicForm.removeControl(elem.name); + }); + } } diff --git a/src/platform/dynamic-forms/services/dynamic-forms.service.ts b/src/platform/dynamic-forms/services/dynamic-forms.service.ts index 3877ec5fab..f97026c003 100644 --- a/src/platform/dynamic-forms/services/dynamic-forms.service.ts +++ b/src/platform/dynamic-forms/services/dynamic-forms.service.ts @@ -36,6 +36,7 @@ export interface ITdDynamicElementConfig { max?: any; selections?: any[]; default?: any; + flex?: number; } export const DYNAMIC_ELEMENT_NAME_REGEX: RegExp = /^[a-zA-Z]+[a-zA-Z0-9-_]*$/;