diff --git a/README.md b/README.md index 282107a39..b97e3318d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Explore the [**live sample**](http://ng2-dynamic-forms.udos86.de/sample/index.ht - [Form Control Configuration](#form-control-configuration) - [Form Control Events](#form-control-events) - [Updating Form Controls](#updating-form-controls) +- [Dynamic Option and List Population](#dynamic-form-control-option-and-list-population) - [Custom Templates](#custom-templates) - [Custom Validators](#custom-validators) - [Custom Form Controls](#custom-form-controls) @@ -689,6 +690,123 @@ To optimize this you can optionally pass a `DynamicFormComponent` to `detectChan this.formService.detectChanges(this.formComponent); ``` +## Dynamic Option And List Population +A common use case of more complex forms is having select, datalist, checkboxes and radio buttons automatically populate based on another form controls value. NG Dynamic Forms accomplishes this by allowing you to provide a `service`, per form control, which will get invoked when the related form controls value changes. + +### Configuration +To connect a form control one will need to include the `dataProvider` attribute along with a `relation` and `service`. See the definitions below. + +| Name | Description | +| :--- | :--- | +| relation | Similar to how relations work, you provide a rootPath or id to the related control | +| *relation*.id | ID of the input control | +| *relation*.rootPath | The path to the input control from the form group. | +| service | A token representing the service in the DI. | + +```typescript +export function WikiPageForm(): DynamicFormControlModel[] { + return [ + new DynamicInputModel({ + id: 'wiki-title', + label: 'Search Title', + value: '', + validators: { + required: null, + }, + errorMessages: { + required: '{{ label }} is required', + }, + }), + new DynamicSelectModel({ + id: 'wiki-page', + label: 'Wiki Page', + value: '-1', + options: [{value: '-1', label: '-- select a wikipedia page'}], + dataProvider: { + relation: {id: 'wiki-title'}, + service: WikipediaService, + }, + relations: [ + { + match: MATCH_DISABLED, + when: [ + { + rootPath: 'wiki-title', value: '', + }, + ], + }, + ], + validators: { + required: null, + }, + errorMessages: { + required: '{{ label }} is required', + }, + }) + ]; +} +``` +### Data Provider +NG Dynamic forms will look attempt to inject a given service via the token provided by the `dataProvider.service` definition. Your service must implement one of two methods depending on your form control. + +| Control Name | Interface | +| :--- | :--- | +| DynamicInputModel | DynamicFormControlListDataProvider\ | +| DynamicSelectModel | DynamicFormControlOptionDataProvider\ | +| DynamicSelectModel | DynamicFormControlOptionDataProvider\ | +| DynamicRadioModel | DynamicFormControlOptionDataProvider\ | +| DynamicCheckboxModel | DynamicFormControlOptionDataProvider\ | + +```typescript +const WIKI_URL = 'https://en.wikipedia.org/w/api.php'; +const PARAMS = new HttpParams({ + fromObject: { + action: 'opensearch', + format: 'json', + origin: '*', + }, +}); + +@Injectable({ + providedIn: 'root', +}) +export class WikipediaService + implements DynamicFormControlListDataProvider, DynamicFormControlOptionDataProvider { + constructor(private http: HttpClient) {} + + fetchList(value: string): Observable { + return this.fetch(value); + } + + fetchOptions(value: string): Observable[]> { + return this.fetch(value).pipe(map( + response => { + if (!Array.isArray(response)) { + return []; + } + + return response.map((val) => { + return { + label: val, + value: val as string, + } as DynamicFormOptionConfig; + }); + }), + ); + } + + fetch(term: string): Observable { + if (typeof(term) !== 'string' || term === '') { + return of([]); + } + + return this.http + .get(WIKI_URL, {params: PARAMS.set('search', term)}).pipe( + map(response => response[1]), + ); + } +} +``` ## Custom Templates diff --git a/projects/ng-dynamic-forms/core/src/lib/component/dynamic-form-control-container.component.ts b/projects/ng-dynamic-forms/core/src/lib/component/dynamic-form-control-container.component.ts index 921b6148e..06ba9af9e 100644 --- a/projects/ng-dynamic-forms/core/src/lib/component/dynamic-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/core/src/lib/component/dynamic-form-control-container.component.ts @@ -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 { @@ -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) { @@ -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)); + } } } diff --git a/projects/ng-dynamic-forms/core/src/lib/core.module.ts b/projects/ng-dynamic-forms/core/src/lib/core.module.ts index f8167a6c5..4930951ee 100644 --- a/projects/ng-dynamic-forms/core/src/lib/core.module.ts +++ b/projects/ng-dynamic-forms/core/src/lib/core.module.ts @@ -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: [ @@ -35,7 +36,8 @@ export class DynamicFormsCoreModule { DynamicFormLayoutService, DynamicFormValidationService, DynamicFormComponentService, - DynamicFormRelationService + DynamicFormRelationService, + DynamicFormDataService, ] }; } diff --git a/projects/ng-dynamic-forms/core/src/lib/core.ts b/projects/ng-dynamic-forms/core/src/lib/core.ts index c214a83ca..be0172656 100644 --- a/projects/ng-dynamic-forms/core/src/lib/core.ts +++ b/projects/ng-dynamic-forms/core/src/lib/core.ts @@ -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"; @@ -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"; diff --git a/projects/ng-dynamic-forms/core/src/lib/model/dynamic-form-control.model.ts b/projects/ng-dynamic-forms/core/src/lib/model/dynamic-form-control.model.ts index 3433b31e3..2eac91db1 100644 --- a/projects/ng-dynamic-forms/core/src/lib/model/dynamic-form-control.model.ts +++ b/projects/ng-dynamic-forms/core/src/lib/model/dynamic-form-control.model.ts @@ -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 { @@ -20,6 +21,7 @@ export interface DynamicFormControlModelConfig { relations?: DynamicFormControlRelation[]; updateOn?: DynamicFormHook; validators?: DynamicValidatorsConfig; + dataProvider?: DynamicFormControlDataConfig; } export abstract class DynamicFormControlModel implements DynamicPathable { @@ -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; @@ -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 { diff --git a/projects/ng-dynamic-forms/core/src/lib/model/misc/dynamic-form-control-data.model.ts b/projects/ng-dynamic-forms/core/src/lib/model/misc/dynamic-form-control-data.model.ts new file mode 100644 index 000000000..737759782 --- /dev/null +++ b/projects/ng-dynamic-forms/core/src/lib/model/misc/dynamic-form-control-data.model.ts @@ -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 { + fetchList(value: string): Observable; +} + +export interface DynamicFormControlOptionDataProvider { + fetchOptions(value: string): Observable[]>; +} diff --git a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-data.service.spec.ts b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-data.service.spec.ts new file mode 100644 index 000000000..662baf4a1 --- /dev/null +++ b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-data.service.spec.ts @@ -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, DynamicFormControlOptionDataProvider { + fetchList(value: string): Observable { + return of(['test']); + } + + fetchOptions(value: string): Observable[]> { + 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 = 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 = 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 = 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); + }); +}); diff --git a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-data.service.ts b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-data.service.ts new file mode 100644 index 000000000..c2dd12f7d --- /dev/null +++ b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-data.service.ts @@ -0,0 +1,85 @@ +import { Injectable, Injector } from "@angular/core"; +import { AbstractControl, FormControl, FormGroup } from "@angular/forms"; +import { DynamicFormControlModel } from "../model/dynamic-form-control.model"; +import { debounceTime, startWith } from "rxjs/operators"; +import { Subscription } from "rxjs"; +import { DynamicInputModel } from '../model/input/dynamic-input.model'; +import { DynamicFormService } from './dynamic-form.service'; +import { DynamicOptionControlModel } from '../model/dynamic-option-control.model'; +import { + DynamicFormControlListDataProvider, + DynamicFormControlOptionDataProvider +} from '../model/misc/dynamic-form-control-data.model'; + +@Injectable({ + providedIn: "root" +}) +export class DynamicFormDataService { + + constructor(private injector: Injector) { + } + + getRelatedFormControl(model: DynamicFormControlModel, group: FormGroup): AbstractControl { + const relation = model.dataProvider.relation; + const control = relation.rootPath ? group.root.get(relation.rootPath) : group.get(relation.id); + + if (!(control instanceof FormControl)) { + console.warn(`No related form control with id ${relation.id} could be found`); + } + + return control; + } + + connectDynamicFormControls(model: DynamicFormControlModel, group: FormGroup): Subscription { + const relatedControl = this.getRelatedFormControl(model, group); + const valueChanges = relatedControl.valueChanges.pipe(startWith(relatedControl.value)); + + return valueChanges + .pipe(debounceTime(400)) + .subscribe((value) => { + if (model instanceof DynamicInputModel) { + this.populateList(value, model, this.injector); + } else if (model instanceof DynamicOptionControlModel) { + this.populateOptions(value, model, this.injector); + } + }); + } + + isListProvider(provider: DynamicFormControlListDataProvider): boolean { + return provider.fetchList !== undefined; + } + + isOptionProvider(provider: DynamicFormControlOptionDataProvider): boolean { + return provider.fetchOptions !== undefined; + } + + populateList(value, model, injector): void { + const provider = injector.get(model.dataProvider.service); + + if (!this.isListProvider(provider)) { + console.warn(`Data Service does not conform to DynamicFormControlListDataProvider interface for id ${model.id}`) + return; + } + + provider.fetchList(value) + .subscribe((val) => { + model.list = val; + injector.get(DynamicFormService).detectChanges(); + }); + } + + populateOptions(value, model, injector): void { + const provider = injector.get(model.dataProvider.service); + + if (!this.isOptionProvider(provider)) { + console.warn(`Data Service does not conform to DynamicFormControlOptionDataProvider interface for id ${model.id}`) + return; + } + + provider.fetchOptions(value) + .subscribe((val) => { + model.options = val; + injector.get(DynamicFormService).detectChanges(); + }); + } +} diff --git a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.spec.ts b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.spec.ts index 37d407aa4..79c8e564f 100644 --- a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.spec.ts +++ b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.spec.ts @@ -1,7 +1,11 @@ import { TestBed, inject } from "@angular/core/testing"; import { ReactiveFormsModule, FormControl, NG_VALIDATORS, NG_ASYNC_VALIDATORS, ValidationErrors, Validators } from "@angular/forms"; import { DynamicFormValidationService } from "./dynamic-form-validation.service"; -import { DYNAMIC_VALIDATORS, Validator, ValidatorFactory } from "./dynamic-form-validators"; +import { + DYNAMIC_GLOBAL_ERROR_MESSAGES, DYNAMIC_VALIDATORS, + Validator, ValidatorErrorMessageFn, + ValidatorFactory, +} from "./dynamic-form-validators"; import { DynamicFormControlModel } from "../model/dynamic-form-control.model"; import { DynamicInputModel } from "../model/input/dynamic-input.model"; import { isFunction } from "../utils/core.utils"; @@ -45,6 +49,14 @@ describe("DynamicFormValidationService test suite", () => { useValue: new Map([ ["testValidatorFactory", testValidatorFactory] ]) + }, + { + provide: DYNAMIC_GLOBAL_ERROR_MESSAGES, + useValue: new Map([ + ['testDynamicError', 'this is a test'], + ['testFunc', (model: DynamicFormControlModel, error: string) => error], + ['*', 'this is a catch-all'], + ]), } ] }); @@ -163,7 +175,9 @@ describe("DynamicFormValidationService test suite", () => { required: "Field is required", minLength: "Field must contain at least {{ minLength }} characters", custom1: "Field {{ id }} has a custom error", - custom2: "Field has a custom error: {{ validator.param }}" + custom2: "Field has a custom error: {{ validator.param }}", + customFunc: (model: DynamicFormControlModel, error: string) => error, + '*': 'catch-all', } }); @@ -190,6 +204,18 @@ describe("DynamicFormValidationService test suite", () => { errorMessages = service.createErrorMessages(testControl, testModel); expect(errorMessages.length).toBe(1); expect(errorMessages[0]).toEqual("Field has a custom error: 42"); + + testControl.setErrors({customFunc: 'error message'}); + + errorMessages = service.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0]).toEqual("error message"); + + testControl.setErrors({unknownToken: true}); + + errorMessages = service.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0]).toEqual("catch-all"); }); @@ -223,4 +249,70 @@ describe("DynamicFormValidationService test suite", () => { expect(service.isFormHook("change")).toBe(true); expect(service.isFormHook("submit")).toBe(true); }); + + it("can create global error messages", () => { + inject([DynamicFormValidationService], + (validationService: DynamicFormValidationService) => { + const testControl: FormControl = new FormControl(); + const testModel: DynamicFormControlModel = new DynamicInputModel({ + id: "testModel", + minLength: 5, + }); + + let errorMessages; + + errorMessages = validationService.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(0); + + testControl.setErrors({testDynamicError: true}); + + errorMessages = validationService.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0]).toEqual("this is a test"); + }); + }); + + it("error messages can be functions", () => { + inject([DynamicFormValidationService], + (validationService: DynamicFormValidationService) => { + const testControl: FormControl = new FormControl(); + const testModel: DynamicFormControlModel = new DynamicInputModel({ + id: "testModel", + minLength: 5, + }); + + let errorMessages; + + errorMessages = validationService.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(0); + + testControl.setErrors({testFunc: 'this should echo'}); + + errorMessages = validationService.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0]).toEqual("this should echo"); + }); + }); + + it("error messages can be catch-alls", () => { + inject([DynamicFormValidationService], + (validationService: DynamicFormValidationService) => { + const testControl: FormControl = new FormControl(); + const testModel: DynamicFormControlModel = new DynamicInputModel({ + id: "testModel", + minLength: 5, + }); + + let errorMessages; + + errorMessages = service.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(0); + + testControl.setErrors({unknown: 'this should not echo'}); + + errorMessages = service.createErrorMessages(testControl, testModel); + expect(errorMessages.length).toBe(1); + expect(errorMessages[0]).toEqual("this is a catch-all"); + }); + }); }); diff --git a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.ts b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.ts index 74cabbb30..a9a7c535a 100644 --- a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.ts +++ b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validation.service.ts @@ -5,20 +5,27 @@ import { ValidatorFn, Validators, NG_VALIDATORS, - NG_ASYNC_VALIDATORS + NG_ASYNC_VALIDATORS, } from "@angular/forms"; import { DynamicFormControlModel } from "../model/dynamic-form-control.model"; import { DynamicFormHook, DynamicValidatorDescriptor, - DynamicValidatorsConfig + DynamicValidatorsConfig, } from "../model/misc/dynamic-form-control-validation.model"; import { isObject, isString } from "../utils/core.utils"; -import { DYNAMIC_VALIDATORS, Validator, ValidatorFactory, ValidatorsToken } from "./dynamic-form-validators"; +import { + DYNAMIC_VALIDATORS, + DYNAMIC_GLOBAL_ERROR_MESSAGES, + Validator, + ValidatorFactory, + ValidatorsToken, + ValidatorErrorMessagesMap, +} from "./dynamic-form-validators"; import { DEFAULT_ERROR_STATE_MATCHER, DYNAMIC_ERROR_MESSAGES_MATCHER, - DynamicErrorMessagesMatcher + DynamicErrorMessagesMatcher, } from "./dynamic-form-validation-matchers"; @Injectable({ @@ -29,7 +36,8 @@ export class DynamicFormValidationService { constructor(@Optional() @Inject(NG_VALIDATORS) private _NG_VALIDATORS: ValidatorFn[], @Optional() @Inject(NG_ASYNC_VALIDATORS) private _NG_ASYNC_VALIDATORS: AsyncValidatorFn[], @Optional() @Inject(DYNAMIC_VALIDATORS) private _DYNAMIC_VALIDATORS: Map, - @Optional() @Inject(DYNAMIC_ERROR_MESSAGES_MATCHER) private _DYNAMIC_ERROR_MESSAGES_MATCHER: DynamicErrorMessagesMatcher) { + @Optional() @Inject(DYNAMIC_ERROR_MESSAGES_MATCHER) private _DYNAMIC_ERROR_MESSAGES_MATCHER: DynamicErrorMessagesMatcher, + @Optional() @Inject(DYNAMIC_GLOBAL_ERROR_MESSAGES) private _DYNAMIC_GLOBAL_ERROR_MESSAGES: ValidatorErrorMessagesMap) { } private getValidatorFn(validatorName: string, validatorArgs: any = null, @@ -169,7 +177,11 @@ export class DynamicFormValidationService { if (model.hasErrorMessages) { - const messagesConfig = model.errorMessages as DynamicValidatorsConfig; + const messagesConfig = Object.assign( + {}, + this._DYNAMIC_GLOBAL_ERROR_MESSAGES || {}, + model.errorMessages + ); Object.keys(control.errors || {}).forEach(validationErrorKey => { @@ -179,12 +191,15 @@ export class DynamicFormValidationService { messageKey = messageKey.replace("length", "Length"); } - if (messagesConfig.hasOwnProperty(messageKey)) { - - const validationError = control.getError(validationErrorKey); - const messageTemplate = messagesConfig[messageKey] as string; + if (messagesConfig.hasOwnProperty(messageKey) || messagesConfig.hasOwnProperty('*')) { + const messageTemplate = messagesConfig[messageKey] || messagesConfig['*']; + const validationError = control.getError(validationErrorKey); + if (typeof (messageTemplate) === 'function') { + messages.push(messageTemplate(model, validationError)); + } else { messages.push(this.parseErrorMessageConfig(messageTemplate, model, validationError)); + } } }); } diff --git a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validators.ts b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validators.ts index d78533e1e..1f4c93391 100644 --- a/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validators.ts +++ b/projects/ng-dynamic-forms/core/src/lib/service/dynamic-form-validators.ts @@ -1,5 +1,6 @@ -import { InjectionToken } from "@angular/core"; -import { AsyncValidatorFn, ValidatorFn } from "@angular/forms"; +import {InjectionToken} from "@angular/core"; +import {AsyncValidatorFn, ValidatorFn} from "@angular/forms"; +import {DynamicFormControlModel} from '../model/dynamic-form-control.model'; export type Validator = ValidatorFn | AsyncValidatorFn; @@ -9,4 +10,10 @@ export type ValidatorsToken = Validator[]; export type ValidatorsMap = Map; +export type ValidatorErrorMessageFn = (model: DynamicFormControlModel, error: any) => string; + +export type ValidatorErrorMessagesMap = Map; + export const DYNAMIC_VALIDATORS = new InjectionToken("DYNAMIC_VALIDATORS"); + +export const DYNAMIC_GLOBAL_ERROR_MESSAGES = new InjectionToken("DYNAMIC_ERROR_MESSAGES"); diff --git a/projects/ng-dynamic-forms/ui-basic/src/lib/dynamic-basic-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-basic/src/lib/dynamic-basic-form-control-container.component.ts index 87aa10584..307a3b250 100644 --- a/projects/ng-dynamic-forms/ui-basic/src/lib/dynamic-basic-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-basic/src/lib/dynamic-basic-form-control-container.component.ts @@ -32,7 +32,8 @@ import { DynamicFormLayoutService, DynamicFormRelationService, DynamicFormValidationService, - DynamicTemplateDirective + DynamicTemplateDirective, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicBasicCheckboxComponent } from "./checkbox/dynamic-basic-checkbox.component"; import { DynamicBasicInputComponent } from "./input/dynamic-basic-input.component"; @@ -71,9 +72,10 @@ export class DynamicBasicFormControlContainerComponent extends DynamicFormContro protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null { diff --git a/projects/ng-dynamic-forms/ui-bootstrap/src/lib/dynamic-bootstrap-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-bootstrap/src/lib/dynamic-bootstrap-form-control-container.component.ts index 78009ee22..04d61fd23 100644 --- a/projects/ng-dynamic-forms/ui-bootstrap/src/lib/dynamic-bootstrap-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-bootstrap/src/lib/dynamic-bootstrap-form-control-container.component.ts @@ -35,7 +35,8 @@ import { DynamicFormLayoutService, DynamicFormRelationService, DynamicFormValidationService, - DynamicTemplateDirective + DynamicTemplateDirective, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicBootstrapCheckboxComponent } from "./checkbox/dynamic-bootstrap-checkbox.component"; import { DynamicBootstrapDatePickerComponent } from "./datepicker/dynamic-bootstrap-datepicker.component"; @@ -83,9 +84,10 @@ export class DynamicBootstrapFormControlContainerComponent extends DynamicFormCo protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } } diff --git a/projects/ng-dynamic-forms/ui-foundation/src/lib/dynamic-foundation-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-foundation/src/lib/dynamic-foundation-form-control-container.component.ts index 28de6f34e..f6fe70dee 100644 --- a/projects/ng-dynamic-forms/ui-foundation/src/lib/dynamic-foundation-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-foundation/src/lib/dynamic-foundation-form-control-container.component.ts @@ -34,7 +34,8 @@ import { DynamicFormLayoutService, DynamicFormRelationService, DynamicFormValidationService, - DynamicTemplateDirective + DynamicTemplateDirective, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicFoundationTextAreaComponent } from "./textarea/dynamic-foundation-textarea.component"; import { DynamicFoundationSwitchComponent } from "./switch/dynamic-foundation-switch.component"; @@ -74,9 +75,10 @@ export class DynamicFoundationFormControlContainerComponent extends DynamicFormC protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null { diff --git a/projects/ng-dynamic-forms/ui-ionic/src/lib/dynamic-ionic-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-ionic/src/lib/dynamic-ionic-form-control-container.component.ts index c58edb629..87f820b37 100644 --- a/projects/ng-dynamic-forms/ui-ionic/src/lib/dynamic-ionic-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-ionic/src/lib/dynamic-ionic-form-control-container.component.ts @@ -37,7 +37,8 @@ import { DynamicFormLayoutService, DynamicFormRelationService, DynamicFormValidationService, - DynamicTemplateDirective + DynamicTemplateDirective, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicIonicCheckboxComponent } from "./checkbox/dynamic-ionic-checkbox.component"; import { DynamicIonicDateTimeComponent } from "./datetime/dynamic-ionic-datetime.component"; @@ -80,9 +81,10 @@ export class DynamicIonicFormControlContainerComponent extends DynamicFormContro protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null { diff --git a/projects/ng-dynamic-forms/ui-kendo/src/lib/dynamic-kendo-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-kendo/src/lib/dynamic-kendo-form-control-container.component.ts index e5cb842de..8cc4639b1 100644 --- a/projects/ng-dynamic-forms/ui-kendo/src/lib/dynamic-kendo-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-kendo/src/lib/dynamic-kendo-form-control-container.component.ts @@ -45,7 +45,8 @@ import { DynamicInputModel, DynamicSelectModel, DynamicTemplateDirective, - isString + isString, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicKendoAutoCompleteComponent } from "./autocomplete/dynamic-kendo-autocomplete.component"; import { DynamicKendoCheckboxComponent } from "./checkbox/dynamic-kendo-checkbox.component"; @@ -97,9 +98,10 @@ export class DynamicKendoFormControlContainerComponent extends DynamicFormContro protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null { diff --git a/projects/ng-dynamic-forms/ui-material/src/lib/dynamic-material-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-material/src/lib/dynamic-material-form-control-container.component.ts index 1835ed840..6d482041b 100644 --- a/projects/ng-dynamic-forms/ui-material/src/lib/dynamic-material-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-material/src/lib/dynamic-material-form-control-container.component.ts @@ -38,7 +38,8 @@ import { DynamicFormValidationService, DynamicInputModel, DynamicTemplateDirective, - DynamicFormValueControlModel + DynamicFormValueControlModel, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicMaterialDatePickerComponent } from "./datepicker/dynamic-material-datepicker.component"; import { DynamicMaterialInputComponent } from "./input/dynamic-material-input.component"; @@ -82,9 +83,10 @@ export class DynamicMaterialFormControlContainerComponent extends DynamicFormCon protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null { diff --git a/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form-control-container.component.ts index dfcdd498f..5e715574a 100644 --- a/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form-control-container.component.ts @@ -37,7 +37,8 @@ import { DynamicFormLayoutService, DynamicFormRelationService, DynamicFormValidationService, - DynamicTemplateDirective + DynamicTemplateDirective, + DynamicFormDataService, } from "@ng-dynamic-forms/core"; import { DynamicNGBootstrapCheckboxComponent } from "./checkbox/dynamic-ng-bootstrap-checkbox.component"; import { DynamicNGBootstrapCheckboxGroupComponent } from "./checkbox-group/dynamic-ng-bootstrap-checkbox-group.component"; @@ -84,9 +85,10 @@ export class DynamicNGBootstrapFormControlContainerComponent extends DynamicForm protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null { diff --git a/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form.component.ts b/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form.component.ts index 6e38a7aa2..d1259ebad 100644 --- a/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form.component.ts +++ b/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/dynamic-ng-bootstrap-form.component.ts @@ -35,11 +35,15 @@ export class DynamicNGBootstrapFormComponent extends DynamicFormComponent { @Output() focus: EventEmitter = new EventEmitter(); @Output() ngbEvent: EventEmitter = new EventEmitter(); - - @ContentChildren(DynamicTemplateDirective) templates: QueryList; + @Input("templates") inputTemplateList: QueryList; + @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; @ViewChildren(DynamicNGBootstrapFormControlContainerComponent) components: QueryList; + get templates(): QueryList | undefined { + return this.inputTemplateList !== undefined ? this.inputTemplateList : this.contentTemplateList; + } + constructor(protected changeDetectorRef: ChangeDetectorRef, protected componentService: DynamicFormComponentService) { super(changeDetectorRef, componentService); diff --git a/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/input/dynamic-ng-bootstrap-input.component.html b/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/input/dynamic-ng-bootstrap-input.component.html index 635f60d91..57cf683f1 100644 --- a/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/input/dynamic-ng-bootstrap-input.component.html +++ b/projects/ng-dynamic-forms/ui-ng-bootstrap/src/lib/input/dynamic-ng-bootstrap-input.component.html @@ -31,6 +31,7 @@ [type]="model.inputType" (blur)="onBlur($event)" (change)="onChange($event)" + (input)="onChange($event)" (focus)="onFocus($event)"/>
diff --git a/projects/ng-dynamic-forms/ui-ngx-bootstrap/src/lib/dynamic-ngx-bootstrap-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-ngx-bootstrap/src/lib/dynamic-ngx-bootstrap-form-control-container.component.ts index f66ebed71..d6f02550e 100644 --- a/projects/ng-dynamic-forms/ui-ngx-bootstrap/src/lib/dynamic-ngx-bootstrap-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-ngx-bootstrap/src/lib/dynamic-ngx-bootstrap-form-control-container.component.ts @@ -32,6 +32,7 @@ import { DynamicFormControlContainerComponent, DynamicFormControlEvent, DynamicFormControlModel, + DynamicFormDataService, DynamicFormLayout, DynamicFormLayoutService, DynamicFormRelationService, @@ -85,9 +86,10 @@ export class DynamicNGxBootstrapFormControlContainerComponent extends DynamicFor protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } } diff --git a/projects/ng-dynamic-forms/ui-primeng/src/lib/dynamic-primeng-form-control-container.component.ts b/projects/ng-dynamic-forms/ui-primeng/src/lib/dynamic-primeng-form-control-container.component.ts index 1fd377234..116bbb232 100644 --- a/projects/ng-dynamic-forms/ui-primeng/src/lib/dynamic-primeng-form-control-container.component.ts +++ b/projects/ng-dynamic-forms/ui-primeng/src/lib/dynamic-primeng-form-control-container.component.ts @@ -37,6 +37,7 @@ import { DynamicFormControlContainerComponent, DynamicFormControlEvent, DynamicFormControlModel, + DynamicFormDataService, DynamicFormLayout, DynamicFormLayoutService, DynamicFormRelationService, @@ -94,9 +95,10 @@ export class DynamicPrimeNGFormControlContainerComponent extends DynamicFormCont protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, protected componentService: DynamicFormComponentService, - protected relationService: DynamicFormRelationService) { + protected relationService: DynamicFormRelationService, + protected dataService: DynamicFormDataService) { - super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService); + super(changeDetectorRef, componentFactoryResolver, layoutService, validationService, componentService, relationService, dataService); } get componentType(): Type | null {