diff options
Diffstat (limited to 'client/src/app/shared/shared-forms')
6 files changed, 123 insertions, 83 deletions
diff --git a/client/src/app/shared/shared-forms/form-reactive.service.ts b/client/src/app/shared/shared-forms/form-reactive.service.ts new file mode 100644 index 000000000..f1b7e0ef2 --- /dev/null +++ b/client/src/app/shared/shared-forms/form-reactive.service.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { AbstractControl, FormGroup } from '@angular/forms' | ||
3 | import { wait } from '@root-helpers/utils' | ||
4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | ||
5 | import { FormValidatorService } from './form-validator.service' | ||
6 | |||
7 | export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } | ||
8 | export type FormReactiveValidationMessages = { | ||
9 | [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages | ||
10 | } | ||
11 | |||
12 | @Injectable() | ||
13 | export class FormReactiveService { | ||
14 | |||
15 | constructor (private formValidatorService: FormValidatorService) { | ||
16 | |||
17 | } | ||
18 | |||
19 | buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { | ||
20 | const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues) | ||
21 | |||
22 | form.statusChanges.subscribe(async () => { | ||
23 | // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed | ||
24 | await this.waitPendingCheck(form) | ||
25 | |||
26 | this.onStatusChanged({ form, formErrors, validationMessages }) | ||
27 | }) | ||
28 | |||
29 | return { form, formErrors, validationMessages } | ||
30 | } | ||
31 | |||
32 | async waitPendingCheck (form: FormGroup) { | ||
33 | if (form.status !== 'PENDING') return | ||
34 | |||
35 | // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519 | ||
36 | // return firstValueFrom(form.statusChanges.pipe(filter(status => status !== 'PENDING'))) | ||
37 | // So we have to fallback to active wait :/ | ||
38 | |||
39 | do { | ||
40 | await wait(10) | ||
41 | } while (form.status === 'PENDING') | ||
42 | } | ||
43 | |||
44 | markAllAsDirty (controlsArg: { [ key: string ]: AbstractControl }) { | ||
45 | const controls = controlsArg | ||
46 | |||
47 | for (const key of Object.keys(controls)) { | ||
48 | const control = controls[key] | ||
49 | |||
50 | if (control instanceof FormGroup) { | ||
51 | this.markAllAsDirty(control.controls) | ||
52 | continue | ||
53 | } | ||
54 | |||
55 | control.markAsDirty() | ||
56 | } | ||
57 | } | ||
58 | |||
59 | forceCheck (form: FormGroup, formErrors: any, validationMessages: FormReactiveValidationMessages) { | ||
60 | this.onStatusChanged({ form, formErrors, validationMessages, onlyDirty: false }) | ||
61 | } | ||
62 | |||
63 | private onStatusChanged (options: { | ||
64 | form: FormGroup | ||
65 | formErrors: FormReactiveErrors | ||
66 | validationMessages: FormReactiveValidationMessages | ||
67 | onlyDirty?: boolean // default true | ||
68 | }) { | ||
69 | const { form, formErrors, validationMessages, onlyDirty = true } = options | ||
70 | |||
71 | for (const field of Object.keys(formErrors)) { | ||
72 | if (formErrors[field] && typeof formErrors[field] === 'object') { | ||
73 | this.onStatusChanged({ | ||
74 | form: form.controls[field] as FormGroup, | ||
75 | formErrors: formErrors[field] as FormReactiveErrors, | ||
76 | validationMessages: validationMessages[field] as FormReactiveValidationMessages, | ||
77 | onlyDirty | ||
78 | }) | ||
79 | |||
80 | continue | ||
81 | } | ||
82 | |||
83 | // clear previous error message (if any) | ||
84 | formErrors[field] = '' | ||
85 | const control = form.get(field) | ||
86 | |||
87 | if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue | ||
88 | |||
89 | const staticMessages = validationMessages[field] | ||
90 | for (const key of Object.keys(control.errors)) { | ||
91 | const formErrorValue = control.errors[key] | ||
92 | |||
93 | // Try to find error message in static validation messages first | ||
94 | // Then check if the validator returns a string that is the error | ||
95 | if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' ' | ||
96 | else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key] | ||
97 | else throw new Error('Form error value of ' + field + ' is invalid') | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | } | ||
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts index a19ffdd82..d1e7be802 100644 --- a/client/src/app/shared/shared-forms/form-reactive.ts +++ b/client/src/app/shared/shared-forms/form-reactive.ts | |||
@@ -1,16 +1,9 @@ | |||
1 | 1 | import { FormGroup } from '@angular/forms' | |
2 | import { AbstractControl, FormGroup } from '@angular/forms' | ||
3 | import { wait } from '@root-helpers/utils' | ||
4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 2 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
5 | import { FormValidatorService } from './form-validator.service' | 3 | import { FormReactiveService, FormReactiveValidationMessages } from './form-reactive.service' |
6 | |||
7 | export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } | ||
8 | export type FormReactiveValidationMessages = { | ||
9 | [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages | ||
10 | } | ||
11 | 4 | ||
12 | export abstract class FormReactive { | 5 | export abstract class FormReactive { |
13 | protected abstract formValidatorService: FormValidatorService | 6 | protected abstract formReactiveService: FormReactiveService |
14 | protected formChanged = false | 7 | protected formChanged = false |
15 | 8 | ||
16 | form: FormGroup | 9 | form: FormGroup |
@@ -18,86 +11,22 @@ export abstract class FormReactive { | |||
18 | validationMessages: FormReactiveValidationMessages | 11 | validationMessages: FormReactiveValidationMessages |
19 | 12 | ||
20 | buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { | 13 | buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { |
21 | const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues) | 14 | const { formErrors, validationMessages, form } = this.formReactiveService.buildForm(obj, defaultValues) |
22 | 15 | ||
23 | this.form = form | 16 | this.form = form |
24 | this.formErrors = formErrors | 17 | this.formErrors = formErrors |
25 | this.validationMessages = validationMessages | 18 | this.validationMessages = validationMessages |
26 | |||
27 | this.form.statusChanges.subscribe(async () => { | ||
28 | // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed | ||
29 | await this.waitPendingCheck() | ||
30 | |||
31 | this.onStatusChanged(this.form, this.formErrors, this.validationMessages) | ||
32 | }) | ||
33 | } | 19 | } |
34 | 20 | ||
35 | protected async waitPendingCheck () { | 21 | protected async waitPendingCheck () { |
36 | if (this.form.status !== 'PENDING') return | 22 | return this.formReactiveService.waitPendingCheck(this.form) |
37 | |||
38 | // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519 | ||
39 | // return firstValueFrom(this.form.statusChanges.pipe(filter(status => status !== 'PENDING'))) | ||
40 | // So we have to fallback to active wait :/ | ||
41 | |||
42 | do { | ||
43 | await wait(10) | ||
44 | } while (this.form.status === 'PENDING') | ||
45 | } | 23 | } |
46 | 24 | ||
47 | protected markAllAsDirty (controlsArg?: { [ key: string ]: AbstractControl }) { | 25 | protected markAllAsDirty () { |
48 | const controls = controlsArg || this.form.controls | 26 | return this.formReactiveService.markAllAsDirty(this.form.controls) |
49 | |||
50 | for (const key of Object.keys(controls)) { | ||
51 | const control = controls[key] | ||
52 | |||
53 | if (control instanceof FormGroup) { | ||
54 | this.markAllAsDirty(control.controls) | ||
55 | continue | ||
56 | } | ||
57 | |||
58 | control.markAsDirty() | ||
59 | } | ||
60 | } | 27 | } |
61 | 28 | ||
62 | protected forceCheck () { | 29 | protected forceCheck () { |
63 | this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false) | 30 | return this.formReactiveService.forceCheck(this.form, this.formErrors, this.validationMessages) |
64 | } | ||
65 | |||
66 | private onStatusChanged ( | ||
67 | form: FormGroup, | ||
68 | formErrors: FormReactiveErrors, | ||
69 | validationMessages: FormReactiveValidationMessages, | ||
70 | onlyDirty = true | ||
71 | ) { | ||
72 | for (const field of Object.keys(formErrors)) { | ||
73 | if (formErrors[field] && typeof formErrors[field] === 'object') { | ||
74 | this.onStatusChanged( | ||
75 | form.controls[field] as FormGroup, | ||
76 | formErrors[field] as FormReactiveErrors, | ||
77 | validationMessages[field] as FormReactiveValidationMessages, | ||
78 | onlyDirty | ||
79 | ) | ||
80 | continue | ||
81 | } | ||
82 | |||
83 | // clear previous error message (if any) | ||
84 | formErrors[field] = '' | ||
85 | const control = form.get(field) | ||
86 | |||
87 | if (control.dirty) this.formChanged = true | ||
88 | |||
89 | if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue | ||
90 | |||
91 | const staticMessages = validationMessages[field] | ||
92 | for (const key of Object.keys(control.errors)) { | ||
93 | const formErrorValue = control.errors[key] | ||
94 | |||
95 | // Try to find error message in static validation messages first | ||
96 | // Then check if the validator returns a string that is the error | ||
97 | if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' ' | ||
98 | else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key] | ||
99 | else throw new Error('Form error value of ' + field + ' is invalid') | ||
100 | } | ||
101 | } | ||
102 | } | 31 | } |
103 | } | 32 | } |
diff --git a/client/src/app/shared/shared-forms/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts index f67d5bb33..897008242 100644 --- a/client/src/app/shared/shared-forms/form-validator.service.ts +++ b/client/src/app/shared/shared-forms/form-validator.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' | 2 | import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' |
3 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 3 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
4 | import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' | 4 | import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive.service' |
5 | 5 | ||
6 | @Injectable() | 6 | @Injectable() |
7 | export class FormValidatorService { | 7 | export class FormValidatorService { |
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts index 495785e7b..bff9862f2 100644 --- a/client/src/app/shared/shared-forms/index.ts +++ b/client/src/app/shared/shared-forms/index.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | export * from './advanced-input-filter.component' | 1 | export * from './advanced-input-filter.component' |
2 | export * from './form-reactive.service' | ||
2 | export * from './form-reactive' | 3 | export * from './form-reactive' |
3 | export * from './form-validator.service' | 4 | export * from './form-validator.service' |
4 | export * from './form-validator.service' | 5 | export * from './form-validator.service' |
diff --git a/client/src/app/shared/shared-forms/input-text.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts index d667ed663..aa4a1cba8 100644 --- a/client/src/app/shared/shared-forms/input-text.component.ts +++ b/client/src/app/shared/shared-forms/input-text.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | 4 | ||
@@ -15,6 +15,8 @@ import { Notifier } from '@app/core' | |||
15 | ] | 15 | ] |
16 | }) | 16 | }) |
17 | export class InputTextComponent implements ControlValueAccessor { | 17 | export class InputTextComponent implements ControlValueAccessor { |
18 | @ViewChild('input') inputElement: ElementRef | ||
19 | |||
18 | @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined | 20 | @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined |
19 | @Input() value = '' | 21 | @Input() value = '' |
20 | @Input() autocomplete = 'off' | 22 | @Input() autocomplete = 'off' |
@@ -65,4 +67,10 @@ export class InputTextComponent implements ControlValueAccessor { | |||
65 | update () { | 67 | update () { |
66 | this.propagateChange(this.value) | 68 | this.propagateChange(this.value) |
67 | } | 69 | } |
70 | |||
71 | focus () { | ||
72 | const el: HTMLElement = this.inputElement.nativeElement | ||
73 | |||
74 | el.focus({ preventScroll: true }) | ||
75 | } | ||
68 | } | 76 | } |
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts index 81f076db6..628affb56 100644 --- a/client/src/app/shared/shared-forms/shared-form.module.ts +++ b/client/src/app/shared/shared-forms/shared-form.module.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | |||
2 | import { InputMaskModule } from 'primeng/inputmask' | 1 | import { InputMaskModule } from 'primeng/inputmask' |
3 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
@@ -7,6 +6,7 @@ import { SharedGlobalIconModule } from '../shared-icons' | |||
7 | import { SharedMainModule } from '../shared-main/shared-main.module' | 6 | import { SharedMainModule } from '../shared-main/shared-main.module' |
8 | import { AdvancedInputFilterComponent } from './advanced-input-filter.component' | 7 | import { AdvancedInputFilterComponent } from './advanced-input-filter.component' |
9 | import { DynamicFormFieldComponent } from './dynamic-form-field.component' | 8 | import { DynamicFormFieldComponent } from './dynamic-form-field.component' |
9 | import { FormReactiveService } from './form-reactive.service' | ||
10 | import { FormValidatorService } from './form-validator.service' | 10 | import { FormValidatorService } from './form-validator.service' |
11 | import { InputSwitchComponent } from './input-switch.component' | 11 | import { InputSwitchComponent } from './input-switch.component' |
12 | import { InputTextComponent } from './input-text.component' | 12 | import { InputTextComponent } from './input-text.component' |
@@ -96,7 +96,8 @@ import { TimestampInputComponent } from './timestamp-input.component' | |||
96 | ], | 96 | ], |
97 | 97 | ||
98 | providers: [ | 98 | providers: [ |
99 | FormValidatorService | 99 | FormValidatorService, |
100 | FormReactiveService | ||
100 | ] | 101 | ] |
101 | }) | 102 | }) |
102 | export class SharedFormModule { } | 103 | export class SharedFormModule { } |