Skip to content

Commit

Permalink
feat(dynamic-forms): add custom type support (closes #314) (#1212)
Browse files Browse the repository at this point in the history
* refactor(dynamic-forms): remove ngModel usage

this is needed because we were mixing reactive forms with template driven forms
and this usage will be deprecated in angular 7

also used the covalent control value accessor mixin

* chore(): remove AbstractControlValueAccessor code since its not needed

* fix(): form support was not needed in the dynamic elements

since we are injecting the control instance into the underlying element
form support was never needed

* feat(dynamic-forms): add custom type support

add ability to add your own custom type as a dynamic element

* fix(): apply review requests
  • Loading branch information
emoralesb05 authored Aug 14, 2018
1 parent 52d21f2 commit 2dce858
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 15 deletions.
9 changes: 8 additions & 1 deletion src/app/components/components/components.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { componentsRoutes } from './components.routes';

Expand Down Expand Up @@ -70,6 +70,8 @@ import { DocumentationToolsModule } from '../../documentation-tools';

import { ToolbarModule } from '../../components/toolbar/toolbar.module';

import { TdTestDynamicComponent } from './dynamic-forms/dynamic-forms.component';

@NgModule({
declarations: [
ComponentsComponent,
Expand Down Expand Up @@ -101,11 +103,13 @@ import { ToolbarModule } from '../../components/toolbar/toolbar.module';
// External Dependencies
NgxChartsDemoComponent,
NgxTranslateDemoComponent,
TdTestDynamicComponent,
],
imports: [
/** Angular Modules */
CommonModule,
FormsModule,
ReactiveFormsModule,
/** Material Modules */
MatButtonModule,
MatListModule,
Expand Down Expand Up @@ -154,5 +158,8 @@ import { ToolbarModule } from '../../components/toolbar/toolbar.module';
componentsRoutes,
ToolbarModule,
],
entryComponents: [
TdTestDynamicComponent,
],
})
export class ComponentsModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -530,4 +530,83 @@ <h4>Angular Validators</h4>
</mat-card-content>
</mat-card>

<mat-card>
<mat-card-content>
<h3 class="mat-title">Custom Dynamic Elements</h3>
<mat-divider></mat-divider>
<mat-tab-group mat-stretch-tabs dynamicHeight>
<mat-tab>
<ng-template matTabLabel>Demo</ng-template>
<td-dynamic-forms [elements]="customElements">
</td-dynamic-forms>
</mat-tab>
<mat-tab>
<ng-template matTabLabel>Code</ng-template>
<h4>Setup</h4>
<p>Custom Component:</p>
<td-highlight lang="typescript">
<![CDATA[
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
selector: 'td-dynamic-input-test',
template: `<td-chips [items]="selections" [formControl]="control"></td-chips>`,
})
export class TdTestDynamicComponent {

/* control property needed to properly bind the underlying element */
control: FormControl;

/* map any of the properties you passed in the config */
selections: string[] = [];

}
]]>
</td-highlight>
<p>Module imports:</p>
<td-highlight lang="typescript">
<![CDATA[
@NgModule({
declarations: [
...
TdTestDynamicComponent,
],
imports: [
...
CommonModule,
ReactiveFormsModule,
CovalentDynamicFormsModule,
],
entryComponents: [
TdTestDynamicComponent,
],
})
export class ComponentsModule {}
]]>
</td-highlight>
<h4>Usage:</h4>
<p>HTML:</p>
<td-highlight lang="html">
<![CDATA[
<td-dynamic-forms [elements]="customElements">
</td-dynamic-forms>
]]>
</td-highlight>
<p>Typescript:</p>
<td-highlight lang="typescript">
<![CDATA[
customElements: ITdDynamicElementConfig[] = [{
name: 'custom',
type: TdTestDynamicComponent,
default: ['list1'],
selections: ['list1', 'list2', 'list3'],
flex: 100,
}];
]]>
</td-highlight>
</mat-tab>
</mat-tab-group>
</mat-card-content>
</mat-card>
<td-readme-loader resourceUrl="platform/dynamic-forms/README.md"></td-readme-loader>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ import {
TdDynamicType,
} from '@covalent/dynamic-forms';

import { FormControl } from '@angular/forms';

@Component({
selector: 'td-dynamic-input-test',
template: `<td-chips [items]="selections" [formControl]="control"></td-chips>`,
})
export class TdTestDynamicComponent {

control: FormControl;
selections: string[] = [];

}

@Component({
selector: 'dynamic-forms-demo',
styleUrls: ['./dynamic-forms.component.scss'],
Expand Down Expand Up @@ -136,6 +149,14 @@ export class DynamicFormsDemoComponent {
flex: 20,
}];

customElements: ITdDynamicElementConfig[] = [{
name: 'custom',
type: TdTestDynamicComponent,
default: ['list1'],
selections: ['list1', 'list2', 'list3'],
flex: 100,
}];

elementOptions: any[] = [
TdDynamicElement.Input,
TdDynamicType.Number,
Expand Down
16 changes: 15 additions & 1 deletion src/platform/dynamic-forms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Pass an array of javascript objects that implement [ITdDynamicElementConfig] wit
export interface ITdDynamicElementConfig {
label?: string;
name: string;
type: TdDynamicType | TdDynamicElement;
type: TdDynamicType | TdDynamicElement | Type<any>;
required?: boolean;
min?: any;
max?: any;
Expand All @@ -64,6 +64,8 @@ export interface ITdDynamicElementConfig {
}
```

NOTE: For custom types, you need to implement a `[control]` property and use it in your underlying element.

Example for HTML usage:

```html
Expand All @@ -85,6 +87,14 @@ Example for HTML usage:
```typescript
import { ITdDynamicElementConfig, TdDynamicElement, TdDynamicType } from '@covalent/dynamic-forms';
...
/* CUSTOM TYPE */
template: '<label>{{label}}</label><input [formControl]="control">',
})
export class DynamicCustomComponent {
control: FormControl;
label: string;
}
...
})
export class Demo {
elements: ITdDynamicElementConfig[] = [{
Expand Down Expand Up @@ -125,6 +135,10 @@ export class Demo {
name: 'datepicker',
label: 'Date',
type: TdDynamicElement.Datepicker,
}, {
name: 'custom',
label: 'Custom',
type: DynamicCustomComponent,
}];
}
```
Expand Down
5 changes: 3 additions & 2 deletions src/platform/dynamic-forms/dynamic-element.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Directive, Input, HostBinding, OnInit, SimpleChanges, OnChanges, TemplateRef, ChangeDetectorRef } from '@angular/core';
import { Component, Directive, Input, HostBinding, OnInit, SimpleChanges, OnChanges, TemplateRef, ChangeDetectorRef, Type } 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';
Expand Down Expand Up @@ -110,8 +110,9 @@ export class TdDynamicElementComponent extends _TdDynamicElementMixinBase
}

ngOnInit(): void {
let component: any = <any>this.type instanceof Type ? this.type : this._dynamicFormsService.getDynamicElement(this.type);
let ref: ComponentRef<any> = this._componentFactoryResolver.
resolveComponentFactory(this._dynamicFormsService.getDynamicElement(this.type))
resolveComponentFactory(component)
.create(this.childElement.viewContainer.injector);
this.childElement.viewContainer.insert(ref.hostView);
this._instance = ref.instance;
Expand Down
59 changes: 50 additions & 9 deletions src/platform/dynamic-forms/dynamic-forms.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import {
ComponentFixture,
} from '@angular/core/testing';
import 'hammerjs';
import { Component } from '@angular/core';
import { Component, NgModule } from '@angular/core';
import { MatNativeDateModule } from '@angular/material/core';
import { Validators, AbstractControl } from '@angular/forms';
import { Validators, AbstractControl, FormControl, ReactiveFormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser';
import { TdDynamicType, TdDynamicElement, ITdDynamicElementConfig,
Expand All @@ -21,6 +21,7 @@ describe('Component: TdDynamicForms', () => {
NoopAnimationsModule,
MatNativeDateModule,
CovalentDynamicFormsModule,
TdDynamicTestModule,
],
declarations: [
TdDynamicFormsTestComponent,
Expand Down Expand Up @@ -160,7 +161,6 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeTruthy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({first_name: 'name', age: 20, dob: dob}));
});
})));
Expand All @@ -183,7 +183,6 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeFalsy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({password: 'mypwd'}));
});
})));
Expand All @@ -206,7 +205,6 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeFalsy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({password: 'myVeryLongString'}));
});
})));
Expand All @@ -230,7 +228,6 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeTruthy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({password: 'mySuperSecretPw'}));
});
})));
Expand Down Expand Up @@ -258,7 +255,6 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeFalsy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({number: 15}));
});
})));
Expand All @@ -284,7 +280,6 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeFalsy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({hexColor: '#ZZZZZZ'}));
});
})));
Expand Down Expand Up @@ -320,10 +315,30 @@ describe('Component: TdDynamicForms', () => {
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeTruthy();
/* tslint:disable-next-line */
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({hexColor: '#F1F1F1', number: 22}));
});
})));

it('should render dynamic custom element', async(inject([], () => {

let fixture: ComponentFixture<any> = TestBed.createComponent(TdDynamicFormsTestComponent);
let component: TdDynamicFormsTestComponent = fixture.debugElement.componentInstance;

expect(fixture.debugElement.queryAll(By.directive(TdDynamicElementComponent)).length).toBe(0);
component.elements = [{
name: 'custom',
type: TdDynamicTestComponent,
default: 'value',
}];
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(fixture.debugElement.queryAll(By.directive(TdDynamicElementComponent)).length).toBe(1);
let dynamicFormsComponent: TdDynamicFormsComponent =
fixture.debugElement.query(By.directive(TdDynamicFormsComponent)).componentInstance;
expect(dynamicFormsComponent.valid).toBeTruthy();
expect(JSON.stringify(dynamicFormsComponent.value)).toBe(JSON.stringify({custom: 'value'}));
});
})));
});

@Component({
Expand All @@ -334,3 +349,29 @@ describe('Component: TdDynamicForms', () => {
class TdDynamicFormsTestComponent {
elements: ITdDynamicElementConfig[];
}

@Component({
selector: 'td-dynamic-input-test',
template: `<input [formControl]="control">`,
})
export class TdDynamicTestComponent {

control: FormControl;

}

@NgModule({
declarations: [
TdDynamicTestComponent,
],
imports: [
ReactiveFormsModule,
],
exports: [
TdDynamicTestComponent,
],
entryComponents: [
TdDynamicTestComponent,
],
})
export class TdDynamicTestModule {}
4 changes: 2 additions & 2 deletions src/platform/dynamic-forms/services/dynamic-forms.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Provider, SkipSelf, Optional } from '@angular/core';
import { Injectable, Provider, SkipSelf, Optional, Type } from '@angular/core';
import { Validators, ValidatorFn, FormControl, AbstractControl } from '@angular/forms';

import { TdDynamicInputComponent } from '../dynamic-elements/dynamic-input/dynamic-input.component';
Expand Down Expand Up @@ -37,7 +37,7 @@ export interface ITdDynamicElementValidator {
export interface ITdDynamicElementConfig {
label?: string;
name: string;
type: TdDynamicType | TdDynamicElement;
type: TdDynamicType | TdDynamicElement | Type<any>;
required?: boolean;
min?: any;
max?: any;
Expand Down

0 comments on commit 2dce858

Please sign in to comment.