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-_]*$/;