Skip to content

Commit

Permalink
Allow for dynamic list and option data based on related input value
Browse files Browse the repository at this point in the history
  • Loading branch information
Steve Rhoades authored and steverhoades committed Jan 18, 2020
1 parent 360ddb6 commit d9b641e
Show file tree
Hide file tree
Showing 16 changed files with 384 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { isString } from "../utils/core.utils";
import { DynamicFormRelationService } from "../service/dynamic-form-relation.service";
import { DynamicFormGroupComponent } from "./dynamic-form-group.component";
import { DynamicFormArrayComponent } from "./dynamic-form-array.component";
import { DynamicFormDataService } from '../service/dynamic-form-data.service';

export abstract class DynamicFormControlContainerComponent implements OnChanges, OnDestroy {

Expand Down Expand Up @@ -77,7 +78,8 @@ export abstract class DynamicFormControlContainerComponent implements OnChanges,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService,
protected componentService: DynamicFormComponentService,
protected relationService: DynamicFormRelationService) {
protected relationService: DynamicFormRelationService,
protected dataService: DynamicFormDataService) {
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -287,6 +289,10 @@ export abstract class DynamicFormControlContainerComponent implements OnChanges,

this.subscriptions.push(...this.relationService.subscribeRelations(this.model, this.group, this.control));
}

if (this.model.dataProvider) {
this.subscriptions.push(this.dataService.connectDynamicFormControls(this.model, this.group));
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion projects/ng-dynamic-forms/core/src/lib/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DynamicFormLayoutService } from "./service/dynamic-form-layout.service"
import { DynamicFormValidationService } from "./service/dynamic-form-validation.service";
import { DynamicFormComponentService } from "./service/dynamic-form-component.service";
import { DynamicFormRelationService } from "./service/dynamic-form-relation.service";
import { DynamicFormDataService } from './service/dynamic-form-data.service';

@NgModule({
imports: [
Expand Down Expand Up @@ -35,7 +36,8 @@ export class DynamicFormsCoreModule {
DynamicFormLayoutService,
DynamicFormValidationService,
DynamicFormComponentService,
DynamicFormRelationService
DynamicFormRelationService,
DynamicFormDataService,
]
};
}
Expand Down
2 changes: 2 additions & 0 deletions projects/ng-dynamic-forms/core/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export * from "./model/switch/dynamic-switch.model";
export * from "./model/textarea/dynamic-textarea.model";
export * from "./model/timepicker/dynamic-timepicker.model";

export * from "./model/misc/dynamic-form-control-data.model";
export * from "./model/misc/dynamic-form-control-layout.model";
export * from "./model/misc/dynamic-form-control-path.model";
export * from "./model/misc/dynamic-form-control-relation.model";
Expand All @@ -50,6 +51,7 @@ export * from "./service/dynamic-form-validators";

export * from "./service/dynamic-form.service";
export * from "./service/dynamic-form-component.service";
export * from "./service/dynamic-form-data.service";
export * from "./service/dynamic-form-layout.service";
export * from "./service/dynamic-form-relation.service";
export * from "./service/dynamic-form-validation.service";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DynamicFormControlRelation } from "./misc/dynamic-form-control-relation
import { DynamicFormHook, DynamicValidatorsConfig } from "./misc/dynamic-form-control-validation.model";
import { serializable, serialize } from "../decorator/serializable.decorator";
import { isBoolean, isObject, isString } from "../utils/core.utils";
import {DynamicFormControlDataConfig} from './misc/dynamic-form-control-data.model';

export interface DynamicFormControlModelConfig {

Expand All @@ -20,6 +21,7 @@ export interface DynamicFormControlModelConfig {
relations?: DynamicFormControlRelation[];
updateOn?: DynamicFormHook;
validators?: DynamicValidatorsConfig;
dataProvider?: DynamicFormControlDataConfig;
}

export abstract class DynamicFormControlModel implements DynamicPathable {
Expand All @@ -38,6 +40,7 @@ export abstract class DynamicFormControlModel implements DynamicPathable {
@serializable() relations: DynamicFormControlRelation[];
@serializable() updateOn: DynamicFormHook | null;
@serializable() validators: DynamicValidatorsConfig | null;
@serializable() dataProvider: DynamicFormControlDataConfig | null;

private readonly disabled$: BehaviorSubject<boolean>;

Expand All @@ -63,6 +66,7 @@ export abstract class DynamicFormControlModel implements DynamicPathable {
this.disabled$ = new BehaviorSubject(isBoolean(config.disabled) ? config.disabled : false);
this.disabled$.subscribe(disabled => this._disabled = disabled);
this.disabledChanges = this.disabled$.asObservable();
this.dataProvider = config.dataProvider || null;
}

get disabled(): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Observable} from 'rxjs';
import {DynamicFormOptionConfig} from '../dynamic-option-control.model';

export interface DynamicFormControlDataRelation {
rootPath?: string;
id?: string;
}

export interface DynamicFormControlDataConfig {
relation: DynamicFormControlDataRelation;
service: any;
}

export interface DynamicFormControlListDataProvider<T> {
fetchList(value: string): Observable<T[]>;
}

export interface DynamicFormControlOptionDataProvider<T> {
fetchOptions(value: string): Observable<DynamicFormOptionConfig<T>[]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {TestBed, inject, tick, fakeAsync} from "@angular/core/testing";
import {FormGroup, ReactiveFormsModule} from "@angular/forms";
import { DynamicFormService } from "./dynamic-form.service";
import { DynamicSelectModel } from "../model/select/dynamic-select.model";
import { DynamicRadioGroupModel } from "../model/radio/dynamic-radio-group.model";
import {DynamicFormDataService} from './dynamic-form-data.service';
import {
DynamicFormControlListDataProvider,
DynamicFormControlOptionDataProvider
} from '../model/misc/dynamic-form-control-data.model';
import {Observable, of} from 'rxjs';
import {Injectable} from '@angular/core';
import {DynamicInputModel} from '../model/input/dynamic-input.model';
import {DynamicFormGroupModel} from '../model/form-group/dynamic-form-group.model';
import {DynamicFormOptionConfig} from '../model/dynamic-option-control.model';

@Injectable()
class TestProvider implements DynamicFormControlListDataProvider<string>, DynamicFormControlOptionDataProvider<string> {
fetchList(value: string): Observable<string[]> {
return of(['test']);
}

fetchOptions(value: string): Observable<DynamicFormOptionConfig<string>[]> {
return of([{
label: 'Test',
value: 'test'
}]);
}
}

@Injectable()
class InvalidTestProvider {

}

describe("DynamicFormDataService test suite", () => {

let service: DynamicFormDataService,
group: FormGroup,
model: DynamicInputModel = new DynamicInputModel({
id: "testInput2",
list: ['item-1', 'item-2', 'item-3'],
value: "item-1",
dataProvider: {
relation: {
id: 'testInput'
},
service: TestProvider,
}
}),
select: DynamicSelectModel<any> = new DynamicSelectModel({
id: "testSelect",
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
value: "option-1",
dataProvider: {
relation: {
id: 'testInput'
},
service: TestProvider,
}
}),
radio: DynamicRadioGroupModel<any> = new DynamicRadioGroupModel({
id: "testRadioGroup",
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
value: "option-1",
dataProvider: {
relation: {
id: 'testInput'
},
service: TestProvider
}
}),
invalidListProvider: DynamicInputModel = new DynamicInputModel({
id: "invalidListProvider",
list: ['item-1', 'item-2', 'item-3'],
value: "item-1",
dataProvider: {
relation: {
id: 'testInput'
},
service: InvalidTestProvider,
}
}),
invalidOptionProvider: DynamicSelectModel<any> = new DynamicSelectModel({
id: "invalidOptionProvider",
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
value: "option-1",
dataProvider: {
relation: {
id: 'testInput'
},
service: InvalidTestProvider,
}
}),
referenceInvalidControl: DynamicInputModel = new DynamicInputModel({
id: "referenceInvalidControl",
list: ['item-1', 'item-2', 'item-3'],
value: "item-1",
dataProvider: {
relation: {
id: 'not-an-id'
},
service: TestProvider,
}
}),
groupModel = new DynamicFormGroupModel({
id: 'test',
group: [
new DynamicRadioGroupModel({
id: "testRootRadioGroup",
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
value: "option-1",
dataProvider: {
relation: {
id: 'testInput'
},
service: TestProvider
}
}),
]
}),
groupInputTest = new DynamicInputModel({
id: "testInput",
dataProvider: {
relation: {
rootPath: 'test.testRootRadioGroup'
},
service: TestProvider,
}
})
;

beforeEach(() => {

TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
providers: [DynamicFormDataService, TestProvider, InvalidTestProvider]
});
});

beforeEach(inject([DynamicFormDataService, DynamicFormService],
(dataService: DynamicFormDataService, formService: DynamicFormService) => {

service = dataService;

group = formService.createFormGroup([
new DynamicInputModel({id: "testInput"}),
model,
select,
radio,
invalidListProvider,
invalidOptionProvider,
groupModel
]);
}));

it("should get related form control correctly", () => {
const compareControl = group.get('testInput');
const relatedFormControl = service.getRelatedFormControl(model, group);

expect(relatedFormControl).toBe(compareControl);
});

it("should get data from provider on related input value change", fakeAsync(() => {
const triggerControl = group.get('testInput');

service.connectDynamicFormControls(model, group);
triggerControl.setValue('newVal');
tick(401);
model.list$.subscribe((list) => expect(list[0]).toBe('test'));
}));

it("should get data from provider on related select option value change", fakeAsync(() => {
const triggerControl = group.get('testInput');

service.connectDynamicFormControls(select, group);
triggerControl.setValue('newVal');
tick(401);
select.options$.subscribe((options) => expect(options[0].value).toBe('test'));
}));

it("should get data from provider on related radio option value change", fakeAsync(() => {
const triggerControl = group.get('testInput');

service.connectDynamicFormControls(radio, group);
triggerControl.setValue('newVal');
tick(401);
radio.options$.subscribe((options) => expect(options[0].value).toBe('test'));
}));

it("should not fail with invalid provider but receive warning with missing list data provider.", fakeAsync(() => {
const triggerControl = group.get('testInput');

service.connectDynamicFormControls(invalidListProvider, group);
triggerControl.setValue('newVal');
tick(401);
invalidListProvider.list$.subscribe((list) => expect(list[0]).toBe('item-1'));
}));

it("should not fail with invalid provider but receive warning with missing options data provider.", fakeAsync(() => {
const triggerControl = group.get('testInput');

service.connectDynamicFormControls(invalidOptionProvider, group);
triggerControl.setValue('newVal');
tick(401);
invalidOptionProvider.options$.subscribe((options) => expect(options[0].value).toBe('option-1'));
}));

it('should show warning with invalid relatedform control', () => {
const relatedFormControl = service.getRelatedFormControl(referenceInvalidControl, group);
expect(relatedFormControl).toBe(null);
});

it('should get related form control from rootPath', () => {
const compareControl = group.root.get('test.testRootRadioGroup');
const relatedFormControl = service.getRelatedFormControl(groupInputTest, group);

expect(relatedFormControl).toBe(compareControl);
});
});
Loading

0 comments on commit d9b641e

Please sign in to comment.