Skip to content

Commit d8c783c

Browse files
Steve RhoadesSteve Rhoades
authored andcommitted
Allow for dynamic list and option data based on related input value
1 parent 360ddb6 commit d8c783c

File tree

8 files changed

+278
-5
lines changed

8 files changed

+278
-5
lines changed

projects/ng-dynamic-forms/core/src/lib/component/dynamic-form-control-container.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { isString } from "../utils/core.utils";
4444
import { DynamicFormRelationService } from "../service/dynamic-form-relation.service";
4545
import { DynamicFormGroupComponent } from "./dynamic-form-group.component";
4646
import { DynamicFormArrayComponent } from "./dynamic-form-array.component";
47+
import { DynamicFormDataService } from '../service/dynamic-form-data.service';
4748

4849
export abstract class DynamicFormControlContainerComponent implements OnChanges, OnDestroy {
4950

@@ -77,7 +78,8 @@ export abstract class DynamicFormControlContainerComponent implements OnChanges,
7778
protected layoutService: DynamicFormLayoutService,
7879
protected validationService: DynamicFormValidationService,
7980
protected componentService: DynamicFormComponentService,
80-
protected relationService: DynamicFormRelationService) {
81+
protected relationService: DynamicFormRelationService,
82+
protected dataService: DynamicFormDataService) {
8183
}
8284

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

288290
this.subscriptions.push(...this.relationService.subscribeRelations(this.model, this.group, this.control));
289291
}
292+
293+
if (this.model.dataProvider) {
294+
this.subscriptions.push(this.dataService.connectDynamicFormControls(this.model, this.group));
295+
}
290296
}
291297
}
292298

projects/ng-dynamic-forms/core/src/lib/core.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DynamicFormLayoutService } from "./service/dynamic-form-layout.service"
88
import { DynamicFormValidationService } from "./service/dynamic-form-validation.service";
99
import { DynamicFormComponentService } from "./service/dynamic-form-component.service";
1010
import { DynamicFormRelationService } from "./service/dynamic-form-relation.service";
11+
import { DynamicFormDataService } from './service/dynamic-form-data.service';
1112

1213
@NgModule({
1314
imports: [
@@ -35,7 +36,8 @@ export class DynamicFormsCoreModule {
3536
DynamicFormLayoutService,
3637
DynamicFormValidationService,
3738
DynamicFormComponentService,
38-
DynamicFormRelationService
39+
DynamicFormRelationService,
40+
DynamicFormDataService,
3941
]
4042
};
4143
}

projects/ng-dynamic-forms/core/src/lib/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export * from "./model/switch/dynamic-switch.model";
3939
export * from "./model/textarea/dynamic-textarea.model";
4040
export * from "./model/timepicker/dynamic-timepicker.model";
4141

42+
export * from "./model/misc/dynamic-form-control-data.model";
4243
export * from "./model/misc/dynamic-form-control-layout.model";
4344
export * from "./model/misc/dynamic-form-control-path.model";
4445
export * from "./model/misc/dynamic-form-control-relation.model";
@@ -50,6 +51,7 @@ export * from "./service/dynamic-form-validators";
5051

5152
export * from "./service/dynamic-form.service";
5253
export * from "./service/dynamic-form-component.service";
54+
export * from "./service/dynamic-form-data.service";
5355
export * from "./service/dynamic-form-layout.service";
5456
export * from "./service/dynamic-form-relation.service";
5557
export * from "./service/dynamic-form-validation.service";

projects/ng-dynamic-forms/core/src/lib/model/dynamic-form-control.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { DynamicFormControlRelation } from "./misc/dynamic-form-control-relation
55
import { DynamicFormHook, DynamicValidatorsConfig } from "./misc/dynamic-form-control-validation.model";
66
import { serializable, serialize } from "../decorator/serializable.decorator";
77
import { isBoolean, isObject, isString } from "../utils/core.utils";
8+
import {DynamicFormControlDataConfig} from './misc/dynamic-form-control-data.model';
89

910
export interface DynamicFormControlModelConfig {
1011

@@ -20,6 +21,7 @@ export interface DynamicFormControlModelConfig {
2021
relations?: DynamicFormControlRelation[];
2122
updateOn?: DynamicFormHook;
2223
validators?: DynamicValidatorsConfig;
24+
dataProvider?: DynamicFormControlDataConfig;
2325
}
2426

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

4245
private readonly disabled$: BehaviorSubject<boolean>;
4346

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

6872
get disabled(): boolean {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {Observable} from 'rxjs';
2+
import {DynamicFormOptionConfig} from '../dynamic-option-control.model';
3+
4+
export interface DynamicFormControlDataOptionConfig extends DynamicFormOptionConfig<any> {
5+
}
6+
7+
export interface DynamicFormControlDataRelation {
8+
rootPath?: string;
9+
id?: string;
10+
}
11+
12+
export interface DynamicFormControlDataConfig {
13+
relation: DynamicFormControlDataRelation;
14+
service: any;
15+
}
16+
17+
export interface DynamicFormControlListDataProvider {
18+
fetchList(value: string): Observable<any[]>;
19+
}
20+
21+
export interface DynamicFormControlOptionDataProvider {
22+
fetchOptions(value: string): Observable<DynamicFormControlDataOptionConfig[]>;
23+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {TestBed, inject, tick, fakeAsync} from "@angular/core/testing";
2+
import {FormGroup, ReactiveFormsModule} from "@angular/forms";
3+
import { DynamicFormService } from "./dynamic-form.service";
4+
import { DynamicSelectModel } from "../model/select/dynamic-select.model";
5+
import { DynamicRadioGroupModel } from "../model/radio/dynamic-radio-group.model";
6+
import {DynamicFormDataService} from './dynamic-form-data.service';
7+
import {
8+
DynamicFormControlDataOptionConfig,
9+
DynamicFormControlListDataProvider,
10+
DynamicFormControlOptionDataProvider
11+
} from '../model/misc/dynamic-form-control-data.model';
12+
import {Observable, of} from 'rxjs';
13+
import {Injectable} from '@angular/core';
14+
import {DynamicInputModel} from '../model/input/dynamic-input.model';
15+
16+
@Injectable()
17+
class TestProvider implements DynamicFormControlListDataProvider, DynamicFormControlOptionDataProvider {
18+
fetchList(value: string): Observable<any[]> {
19+
return of(['test']);
20+
}
21+
22+
fetchOptions(value: string): Observable<DynamicFormControlDataOptionConfig[]> {
23+
return of([{
24+
label: 'Test',
25+
value: 'test'
26+
}]);
27+
}
28+
}
29+
30+
@Injectable()
31+
class InvalidTestProvider {
32+
33+
}
34+
35+
describe("DynamicFormDataService test suite", () => {
36+
37+
let service: DynamicFormDataService,
38+
group: FormGroup,
39+
model: DynamicInputModel = new DynamicInputModel({
40+
id: "testInput2",
41+
list: ['item-1', 'item-2', 'item-3'],
42+
value: "item-1",
43+
dataProvider: {
44+
relation: {
45+
id: 'testInput'
46+
},
47+
service: TestProvider,
48+
}
49+
}),
50+
select: DynamicSelectModel<any> = new DynamicSelectModel({
51+
id: "testSelect",
52+
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
53+
value: "option-1",
54+
dataProvider: {
55+
relation: {
56+
id: 'testInput'
57+
},
58+
service: TestProvider,
59+
}
60+
}),
61+
radio: DynamicRadioGroupModel<any> = new DynamicRadioGroupModel({
62+
id: "testRadioGroup",
63+
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
64+
value: "option-1",
65+
dataProvider: {
66+
relation: {
67+
id: 'testInput'
68+
},
69+
service: TestProvider
70+
}
71+
}),
72+
invalidProvider: DynamicInputModel = new DynamicInputModel({
73+
id: "testInput3",
74+
list: ['item-1', 'item-2', 'item-3'],
75+
value: "item-1",
76+
dataProvider: {
77+
relation: {
78+
id: 'testInput'
79+
},
80+
service: InvalidTestProvider,
81+
}
82+
})
83+
;
84+
85+
beforeEach(() => {
86+
87+
TestBed.configureTestingModule({
88+
imports: [ReactiveFormsModule],
89+
providers: [DynamicFormDataService, TestProvider, InvalidTestProvider]
90+
});
91+
});
92+
93+
beforeEach(inject([DynamicFormDataService, DynamicFormService],
94+
(dataService: DynamicFormDataService, formService: DynamicFormService) => {
95+
96+
service = dataService;
97+
98+
group = formService.createFormGroup([
99+
new DynamicInputModel({id: "testInput"}),
100+
model,
101+
select,
102+
radio,
103+
invalidProvider
104+
]);
105+
}));
106+
107+
it("should get related form control correctly", () => {
108+
const compareControl = group.get('testInput');
109+
const relatedFormControl = service.getRelatedFormControl(model, group);
110+
111+
expect(relatedFormControl).toBe(compareControl);
112+
});
113+
114+
it("should get data from provider on related input value change", fakeAsync(() => {
115+
const triggerControl = group.get('testInput');
116+
117+
service.connectDynamicFormControls(model, group);
118+
triggerControl.setValue('newVal');
119+
tick(401);
120+
model.list$.subscribe((list) => expect(list[0]).toBe('test'));
121+
}));
122+
123+
it("should get data from provider on related select option value change", fakeAsync(() => {
124+
const triggerControl = group.get('testInput');
125+
126+
service.connectDynamicFormControls(select, group);
127+
triggerControl.setValue('newVal');
128+
tick(401);
129+
select.options$.subscribe((options) => expect(options[0].value).toBe('test'));
130+
}));
131+
132+
it("should get data from provider on related radio option value change", fakeAsync(() => {
133+
const triggerControl = group.get('testInput');
134+
135+
service.connectDynamicFormControls(radio, group);
136+
triggerControl.setValue('newVal');
137+
tick(401);
138+
radio.options$.subscribe((options) => expect(options[0].value).toBe('test'));
139+
}));
140+
141+
it("should not fail with invalid provider but receive warning.", fakeAsync(() => {
142+
const triggerControl = group.get('testInput');
143+
144+
service.connectDynamicFormControls(invalidProvider, group);
145+
triggerControl.setValue('newVal');
146+
tick(401);
147+
invalidProvider.list$.subscribe((list) => expect(list[0]).toBe('item-1'));
148+
}));
149+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Injectable, Injector } from "@angular/core";
2+
import { AbstractControl, FormControl, FormGroup } from "@angular/forms";
3+
import { DynamicFormControlModel } from "../model/dynamic-form-control.model";
4+
import { debounceTime, startWith } from "rxjs/operators";
5+
import { Subscription } from "rxjs";
6+
import { DynamicInputModel } from '../model/input/dynamic-input.model';
7+
import { DynamicFormService } from './dynamic-form.service';
8+
import { DynamicOptionControlModel } from '../model/dynamic-option-control.model';
9+
import {
10+
DynamicFormControlListDataProvider,
11+
DynamicFormControlOptionDataProvider
12+
} from '../model/misc/dynamic-form-control-data.model';
13+
14+
@Injectable({
15+
providedIn: "root"
16+
})
17+
export class DynamicFormDataService {
18+
19+
constructor(private injector: Injector) {
20+
}
21+
22+
getRelatedFormControl(model: DynamicFormControlModel, group: FormGroup): AbstractControl {
23+
const relation = model.dataProvider.relation;
24+
const control = relation.rootPath ? group.root.get(relation.rootPath) : group.get(relation.id);
25+
26+
if (!(control instanceof FormControl)) {
27+
console.warn(`No related form control with id ${relation.id} could be found`);
28+
}
29+
30+
return control;
31+
}
32+
33+
connectDynamicFormControls(model: DynamicFormControlModel, group: FormGroup): Subscription {
34+
const relatedControl = this.getRelatedFormControl(model, group);
35+
const valueChanges = relatedControl.valueChanges.pipe(startWith(relatedControl.value));
36+
37+
return valueChanges
38+
.pipe(debounceTime(400))
39+
.subscribe((value) => {
40+
if (model instanceof DynamicInputModel) {
41+
this.populateList(value, model, this.injector);
42+
} else if (model instanceof DynamicOptionControlModel) {
43+
this.populateOptions(value, model, this.injector);
44+
}
45+
});
46+
}
47+
48+
isListProvider(provider: DynamicFormControlListDataProvider): boolean {
49+
return provider.fetchList !== undefined;
50+
}
51+
52+
isOptionProvider(provider: DynamicFormControlOptionDataProvider): boolean {
53+
return provider.fetchOptions !== undefined;
54+
}
55+
56+
populateList(value, model, injector): void {
57+
const provider = injector.get(model.dataProvider.service);
58+
59+
if (!this.isListProvider(provider)) {
60+
console.warn(`Data Service does not conform to DynamicFormControlListDataProvider interface for id ${model.id}`)
61+
return;
62+
}
63+
64+
provider.fetchList(value)
65+
.subscribe((val) => {
66+
model.list = val;
67+
injector.get(DynamicFormService).detectChanges();
68+
});
69+
}
70+
71+
populateOptions(value, model, injector): void {
72+
const provider = injector.get(model.dataProvider.service);
73+
74+
if (!this.isOptionProvider(provider)) {
75+
console.warn(`Data Service does not conform to DynamicFormControlOptionDataProvider interface for id ${model.id}`)
76+
return;
77+
}
78+
79+
provider.fetchOptions(value)
80+
.subscribe((val) => {
81+
model.options = val;
82+
injector.get(DynamicFormService).detectChanges();
83+
});
84+
}
85+
}

projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form-control-container.component.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import {
3737
DynamicFormLayoutService,
3838
DynamicFormRelationService,
3939
DynamicFormValidationService,
40-
DynamicTemplateDirective
40+
DynamicTemplateDirective,
41+
DynamicFormDataService,
4142
} from "@ng-dynamic-forms/core";
4243
import { DynamicNGBootstrapCheckboxComponent } from "./checkbox/dynamic-ng-bootstrap-checkbox.component";
4344
import { DynamicNGBootstrapCheckboxGroupComponent } from "./checkbox-group/dynamic-ng-bootstrap-checkbox-group.component";
@@ -84,9 +85,10 @@ export class DynamicNGBootstrapFormControlContainerComponent extends DynamicForm
8485
protected layoutService: DynamicFormLayoutService,
8586
protected validationService: DynamicFormValidationService,
8687
protected componentService: DynamicFormComponentService,
87-
protected relationService: DynamicFormRelationService) {
88+
protected relationService: DynamicFormRelationService,
89+
protected dataService: DynamicFormDataService) {
8890

89-
super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService);
91+
super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService);
9092
}
9193

9294
get componentType(): Type<DynamicFormControl> | null {

0 commit comments

Comments
 (0)