Skip to content

Commit d9b641e

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

16 files changed

+384
-27
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {Observable} from 'rxjs';
2+
import {DynamicFormOptionConfig} from '../dynamic-option-control.model';
3+
4+
export interface DynamicFormControlDataRelation {
5+
rootPath?: string;
6+
id?: string;
7+
}
8+
9+
export interface DynamicFormControlDataConfig {
10+
relation: DynamicFormControlDataRelation;
11+
service: any;
12+
}
13+
14+
export interface DynamicFormControlListDataProvider<T> {
15+
fetchList(value: string): Observable<T[]>;
16+
}
17+
18+
export interface DynamicFormControlOptionDataProvider<T> {
19+
fetchOptions(value: string): Observable<DynamicFormOptionConfig<T>[]>;
20+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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+
DynamicFormControlListDataProvider,
9+
DynamicFormControlOptionDataProvider
10+
} from '../model/misc/dynamic-form-control-data.model';
11+
import {Observable, of} from 'rxjs';
12+
import {Injectable} from '@angular/core';
13+
import {DynamicInputModel} from '../model/input/dynamic-input.model';
14+
import {DynamicFormGroupModel} from '../model/form-group/dynamic-form-group.model';
15+
import {DynamicFormOptionConfig} from '../model/dynamic-option-control.model';
16+
17+
@Injectable()
18+
class TestProvider implements DynamicFormControlListDataProvider<string>, DynamicFormControlOptionDataProvider<string> {
19+
fetchList(value: string): Observable<string[]> {
20+
return of(['test']);
21+
}
22+
23+
fetchOptions(value: string): Observable<DynamicFormOptionConfig<string>[]> {
24+
return of([{
25+
label: 'Test',
26+
value: 'test'
27+
}]);
28+
}
29+
}
30+
31+
@Injectable()
32+
class InvalidTestProvider {
33+
34+
}
35+
36+
describe("DynamicFormDataService test suite", () => {
37+
38+
let service: DynamicFormDataService,
39+
group: FormGroup,
40+
model: DynamicInputModel = new DynamicInputModel({
41+
id: "testInput2",
42+
list: ['item-1', 'item-2', 'item-3'],
43+
value: "item-1",
44+
dataProvider: {
45+
relation: {
46+
id: 'testInput'
47+
},
48+
service: TestProvider,
49+
}
50+
}),
51+
select: DynamicSelectModel<any> = new DynamicSelectModel({
52+
id: "testSelect",
53+
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
54+
value: "option-1",
55+
dataProvider: {
56+
relation: {
57+
id: 'testInput'
58+
},
59+
service: TestProvider,
60+
}
61+
}),
62+
radio: DynamicRadioGroupModel<any> = new DynamicRadioGroupModel({
63+
id: "testRadioGroup",
64+
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
65+
value: "option-1",
66+
dataProvider: {
67+
relation: {
68+
id: 'testInput'
69+
},
70+
service: TestProvider
71+
}
72+
}),
73+
invalidListProvider: DynamicInputModel = new DynamicInputModel({
74+
id: "invalidListProvider",
75+
list: ['item-1', 'item-2', 'item-3'],
76+
value: "item-1",
77+
dataProvider: {
78+
relation: {
79+
id: 'testInput'
80+
},
81+
service: InvalidTestProvider,
82+
}
83+
}),
84+
invalidOptionProvider: DynamicSelectModel<any> = new DynamicSelectModel({
85+
id: "invalidOptionProvider",
86+
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
87+
value: "option-1",
88+
dataProvider: {
89+
relation: {
90+
id: 'testInput'
91+
},
92+
service: InvalidTestProvider,
93+
}
94+
}),
95+
referenceInvalidControl: DynamicInputModel = new DynamicInputModel({
96+
id: "referenceInvalidControl",
97+
list: ['item-1', 'item-2', 'item-3'],
98+
value: "item-1",
99+
dataProvider: {
100+
relation: {
101+
id: 'not-an-id'
102+
},
103+
service: TestProvider,
104+
}
105+
}),
106+
groupModel = new DynamicFormGroupModel({
107+
id: 'test',
108+
group: [
109+
new DynamicRadioGroupModel({
110+
id: "testRootRadioGroup",
111+
options: [{value: "option-1"}, {value: "option-2"}, {value: "option-3"}],
112+
value: "option-1",
113+
dataProvider: {
114+
relation: {
115+
id: 'testInput'
116+
},
117+
service: TestProvider
118+
}
119+
}),
120+
]
121+
}),
122+
groupInputTest = new DynamicInputModel({
123+
id: "testInput",
124+
dataProvider: {
125+
relation: {
126+
rootPath: 'test.testRootRadioGroup'
127+
},
128+
service: TestProvider,
129+
}
130+
})
131+
;
132+
133+
beforeEach(() => {
134+
135+
TestBed.configureTestingModule({
136+
imports: [ReactiveFormsModule],
137+
providers: [DynamicFormDataService, TestProvider, InvalidTestProvider]
138+
});
139+
});
140+
141+
beforeEach(inject([DynamicFormDataService, DynamicFormService],
142+
(dataService: DynamicFormDataService, formService: DynamicFormService) => {
143+
144+
service = dataService;
145+
146+
group = formService.createFormGroup([
147+
new DynamicInputModel({id: "testInput"}),
148+
model,
149+
select,
150+
radio,
151+
invalidListProvider,
152+
invalidOptionProvider,
153+
groupModel
154+
]);
155+
}));
156+
157+
it("should get related form control correctly", () => {
158+
const compareControl = group.get('testInput');
159+
const relatedFormControl = service.getRelatedFormControl(model, group);
160+
161+
expect(relatedFormControl).toBe(compareControl);
162+
});
163+
164+
it("should get data from provider on related input value change", fakeAsync(() => {
165+
const triggerControl = group.get('testInput');
166+
167+
service.connectDynamicFormControls(model, group);
168+
triggerControl.setValue('newVal');
169+
tick(401);
170+
model.list$.subscribe((list) => expect(list[0]).toBe('test'));
171+
}));
172+
173+
it("should get data from provider on related select option value change", fakeAsync(() => {
174+
const triggerControl = group.get('testInput');
175+
176+
service.connectDynamicFormControls(select, group);
177+
triggerControl.setValue('newVal');
178+
tick(401);
179+
select.options$.subscribe((options) => expect(options[0].value).toBe('test'));
180+
}));
181+
182+
it("should get data from provider on related radio option value change", fakeAsync(() => {
183+
const triggerControl = group.get('testInput');
184+
185+
service.connectDynamicFormControls(radio, group);
186+
triggerControl.setValue('newVal');
187+
tick(401);
188+
radio.options$.subscribe((options) => expect(options[0].value).toBe('test'));
189+
}));
190+
191+
it("should not fail with invalid provider but receive warning with missing list data provider.", fakeAsync(() => {
192+
const triggerControl = group.get('testInput');
193+
194+
service.connectDynamicFormControls(invalidListProvider, group);
195+
triggerControl.setValue('newVal');
196+
tick(401);
197+
invalidListProvider.list$.subscribe((list) => expect(list[0]).toBe('item-1'));
198+
}));
199+
200+
it("should not fail with invalid provider but receive warning with missing options data provider.", fakeAsync(() => {
201+
const triggerControl = group.get('testInput');
202+
203+
service.connectDynamicFormControls(invalidOptionProvider, group);
204+
triggerControl.setValue('newVal');
205+
tick(401);
206+
invalidOptionProvider.options$.subscribe((options) => expect(options[0].value).toBe('option-1'));
207+
}));
208+
209+
it('should show warning with invalid relatedform control', () => {
210+
const relatedFormControl = service.getRelatedFormControl(referenceInvalidControl, group);
211+
expect(relatedFormControl).toBe(null);
212+
});
213+
214+
it('should get related form control from rootPath', () => {
215+
const compareControl = group.root.get('test.testRootRadioGroup');
216+
const relatedFormControl = service.getRelatedFormControl(groupInputTest, group);
217+
218+
expect(relatedFormControl).toBe(compareControl);
219+
});
220+
});

0 commit comments

Comments
 (0)