diff --git a/packages/cdk/forms/__tests__/abstractControl.spec.ts b/packages/cdk/forms/__tests__/abstractControl.spec.ts index fb1c6df6b..bbc04a33b 100644 --- a/packages/cdk/forms/__tests__/abstractControl.spec.ts +++ b/packages/cdk/forms/__tests__/abstractControl.spec.ts @@ -5,33 +5,37 @@ import { AsyncValidatorFn, ValidationErrors, ValidatorFn, ValidatorOptions } fro import { Validators } from '../src/validators' class Control extends AbstractControl { - readonly valueRef: Ref = ref(null) + _valueRef: Ref = ref(null) constructor( validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, ) { super(validatorOrOptions, asyncValidator) + + this._initAllStatus() + this._watchEffect() } reset(): void {} setValue(value: T | null): void { - this.valueRef.value = value + this._valueRef.value = value } getValue(): T { - return this.valueRef.value as T + return this._valueRef.value as T } markAsBlurred(): void {} markAsUnblurred(): void {} + markAsDirty(): void {} + markAsPristine(): void {} async validate(): Promise { return this._validate() } - private _watchEffect() { - watch([this.valueRef, this.blurred], () => { + watch([this._valueRef, this._blurred], () => { this._validate() }) - watch(this.errors, errors => { + watch(this._errors, errors => { this._status.value = errors ? 'invalid' : 'valid' }) } @@ -45,20 +49,16 @@ describe('abstractControl.ts', () => { control = new Control() }) - test('init status work', () => { + test('init all status work', () => { expect(control.status.value).toEqual('valid') + expect(control.errors.value).toBeNull() expect(control.valid.value).toEqual(true) expect(control.invalid.value).toEqual(false) expect(control.validating.value).toEqual(false) - }) - - test('init errors work', () => { - expect(control.errors.value).toBeNull() - }) - - test('init blurred work', () => { expect(control.blurred.value).toEqual(false) expect(control.unblurred.value).toEqual(true) + expect(control.dirty.value).toEqual(false) + expect(control.pristine.value).toEqual(true) }) test('init props work', () => { @@ -74,7 +74,7 @@ describe('abstractControl.ts', () => { expect(await control.validate()).toEqual({ required: { message: '' } }) control.setValidator([email, minLength(5)]) - control.valueRef.value = 'test' + control.setValue('test') expect(await control.validate()).toEqual({ email: { message: '', actual: 'test' }, @@ -215,7 +215,7 @@ describe('abstractControl.ts', () => { control = new Control(Validators.required, _asyncValidator) expect(await control.validate()).toEqual({ required: { message: '' } }) - control.valueRef.value = 'test' + control.setValue('test') control.validate() expect(control.status.value).toEqual('validating') diff --git a/packages/cdk/forms/__tests__/formArray.spec.ts b/packages/cdk/forms/__tests__/formArray.spec.ts index 17c39341b..6aa5323cc 100644 --- a/packages/cdk/forms/__tests__/formArray.spec.ts +++ b/packages/cdk/forms/__tests__/formArray.spec.ts @@ -143,18 +143,28 @@ describe('formArray.ts', () => { test('markAsBlurred and markAsUnblurred work', async () => { array.markAsBlurred() - await flushPromises() expect(array.blurred.value).toEqual(true) array.markAsUnblurred() - await flushPromises() expect(array.blurred.value).toEqual(false) }) + test('markAsDirty and markAsPristine work', async () => { + array.markAsDirty() + await flushPromises() + + expect(array.dirty.value).toEqual(true) + + array.markAsPristine() + await flushPromises() + + expect(array.dirty.value).toEqual(false) + }) + test('validate work', async () => { expect(await array.validate()).toBeNull() @@ -187,9 +197,6 @@ describe('formArray.ts', () => { test('default change work', async () => { array = new FormArray([newFormGroup()], { validators: _validator }) - array.setValue([{ control: '1234' }]) - await flushPromises() - expect(array.invalid.value).toEqual(true) expect(array.hasError('test')).toEqual(true) @@ -198,28 +205,37 @@ describe('formArray.ts', () => { expect(array.invalid.value).toEqual(false) expect(array.hasError('test')).toEqual(false) + + array.setValue([{ control: '1234' }]) + await flushPromises() + + expect(array.invalid.value).toEqual(true) + expect(array.hasError('test')).toEqual(true) }) test('blur trigger validate work', async () => { array = new FormArray([newFormGroup()], { trigger: 'blur', validators: _validator }) - array.setValue([{ control: '1234' }]) - await flushPromises() - - expect(array.invalid.value).toEqual(false) - expect(array.hasError('test')).toEqual(false) + expect(array.invalid.value).toEqual(true) + expect(array.hasError('test')).toEqual(true) - array.markAsBlurred() + array.setValue([{ control: 'test' }]) await flushPromises() expect(array.invalid.value).toEqual(true) expect(array.hasError('test')).toEqual(true) - array.setValue([{ control: 'test' }]) + array.markAsBlurred() await flushPromises() expect(array.invalid.value).toEqual(false) expect(array.hasError('test')).toEqual(false) + + array.setValue([{ control: '1234' }]) + await flushPromises() + + expect(array.invalid.value).toEqual(true) + expect(array.hasError('test')).toEqual(true) }) test('submit trigger validate work', async () => { @@ -237,23 +253,17 @@ describe('formArray.ts', () => { { trigger: 'submit', validators: _validator }, ) - array.setValue([{ control: '' }]) - array.markAsBlurred() - await flushPromises() - - expect(array.invalid.value).toEqual(false) - expect(array.hasError('test')).toEqual(false) - expect(array.hasError('required', [0, 'control'])).toEqual(false) - expect(array.hasError('async', [0, 'group', 'control'])).toEqual(false) - - await array.validate() - expect(array.invalid.value).toEqual(true) expect(array.hasError('test')).toEqual(true) expect(array.hasError('required', [0, 'control'])).toEqual(true) + expect(array.hasError('async', [0, 'group', 'control'])).toEqual(false) + + await flushPromises() + expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true) array.setValue([{ control: 'test' }]) + array.markAsBlurred() await flushPromises() expect(array.invalid.value).toEqual(true) @@ -267,6 +277,21 @@ describe('formArray.ts', () => { expect(array.hasError('test')).toEqual(false) expect(array.hasError('required', [0, 'control'])).toEqual(false) expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true) + + array.setValue([{ control: '1234' }]) + await flushPromises() + + expect(array.invalid.value).toEqual(true) + expect(array.hasError('test')).toEqual(false) + expect(array.hasError('required', [0, 'control'])).toEqual(false) + expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true) + + await array.validate() + + expect(array.invalid.value).toEqual(true) + expect(array.hasError('test')).toEqual(true) + expect(array.hasError('required', [0, 'control'])).toEqual(false) + expect(array.hasError('async', [0, 'group', 'control'])).toEqual(true) }) }) }) diff --git a/packages/cdk/forms/__tests__/formControl.spec.ts b/packages/cdk/forms/__tests__/formControl.spec.ts index 701189aca..f24609c97 100644 --- a/packages/cdk/forms/__tests__/formControl.spec.ts +++ b/packages/cdk/forms/__tests__/formControl.spec.ts @@ -1,5 +1,6 @@ import { flushPromises } from '@vue/test-utils' import { FormControl } from '../src/controls/formControl' +import { ValidationErrors } from '../src/types' import { Validators } from '../src/validators' describe('formControl.ts', () => { @@ -13,14 +14,17 @@ describe('formControl.ts', () => { test('reset work', async () => { control.setValue('test') control.markAsBlurred() + control.markAsDirty() expect(control.valueRef.value).toEqual('test') expect(control.blurred.value).toEqual(true) + expect(control.dirty.value).toEqual(true) control.reset() expect(control.valueRef.value).toBeNull() expect(control.blurred.value).toEqual(false) + expect(control.dirty.value).toEqual(false) }) test('setValue and getValue work', () => { @@ -45,6 +49,16 @@ describe('formControl.ts', () => { expect(control.blurred.value).toEqual(false) }) + test('markAsDirty and markAsPristine work', () => { + control.markAsDirty() + + expect(control.dirty.value).toEqual(true) + + control.markAsPristine() + + expect(control.dirty.value).toEqual(false) + }) + test('validate work', async () => { expect(await control.validate()).toBeNull() @@ -58,45 +72,66 @@ describe('formControl.ts', () => { let control: FormControl test('default change work', async () => { - control = new FormControl(null, { validators: Validators.required }) + const _asyncValidator = (value: unknown) => + Promise.resolve(value === 'test' ? null : ({ async: { message: 'async' } } as ValidationErrors)) + + control = new FormControl('test', { validators: Validators.required, asyncValidators: _asyncValidator }) expect(control.hasError('required')).toEqual(false) + expect(control.hasError('async')).toEqual(false) control.setValue('') await flushPromises() expect(control.hasError('required')).toEqual(true) + expect(control.hasError('async')).toEqual(false) + + control.setValue('1234') + await flushPromises() + + expect(control.hasError('required')).toEqual(false) + expect(control.hasError('async')).toEqual(true) }) test('blur trigger validate work', async () => { control = new FormControl(null, { trigger: 'blur', validators: Validators.required }) - expect(control.hasError('required')).toEqual(false) + expect(control.hasError('required')).toEqual(true) - control.markAsBlurred() + control.setValue('test') await flushPromises() expect(control.hasError('required')).toEqual(true) - control.setValue('test') + control.markAsBlurred() await flushPromises() expect(control.hasError('required')).toEqual(false) + + control.setValue('') + await flushPromises() + + expect(control.hasError('required')).toEqual(true) }) test('submit trigger validate work', async () => { control = new FormControl(null, { trigger: 'submit', validators: Validators.required }) - expect(control.hasError('required')).toEqual(false) + expect(control.hasError('required')).toEqual(true) + + control.setValue('test') + await flushPromises() + + expect(control.hasError('required')).toEqual(true) control.markAsBlurred() await flushPromises() - expect(control.hasError('required')).toEqual(false) + expect(control.hasError('required')).toEqual(true) await control.validate() - expect(control.hasError('required')).toEqual(true) + expect(control.hasError('required')).toEqual(false) }) }) }) diff --git a/packages/cdk/forms/__tests__/formGroup.spec.ts b/packages/cdk/forms/__tests__/formGroup.spec.ts index 38044e4f0..1aec657e5 100644 --- a/packages/cdk/forms/__tests__/formGroup.spec.ts +++ b/packages/cdk/forms/__tests__/formGroup.spec.ts @@ -104,18 +104,28 @@ describe('formGroup.ts', () => { test('markAsBlurred and markAsUnblurred work', async () => { group.markAsBlurred() - await flushPromises() expect(group.blurred.value).toEqual(true) group.markAsUnblurred() - await flushPromises() expect(group.blurred.value).toEqual(false) }) + test('markAsDirty and markAsPristine work', async () => { + group.markAsDirty() + await flushPromises() + + expect(group.dirty.value).toEqual(true) + + group.markAsPristine() + await flushPromises() + + expect(group.dirty.value).toEqual(false) + }) + test('validate work', async () => { expect(await group.validate()).toBeNull() @@ -159,13 +169,17 @@ describe('formGroup.ts', () => { }), }) - group.setValue({ control: '' }) - await flushPromises() - expect(group.invalid.value).toEqual(true) expect(group.hasError('required', 'control')).toEqual(true) expect(group.hasError('test')).toEqual(false) + group.setValue({ control: 'test' }) + await flushPromises() + + expect(group.invalid.value).toEqual(false) + expect(group.hasError('required', 'control')).toEqual(false) + expect(group.hasError('test')).toEqual(false) + group.setValidator(_validator) group.setValue({ control: '1234' }) await flushPromises() @@ -197,35 +211,31 @@ describe('formGroup.ts', () => { { trigger: 'blur', validators: _validator }, ) - group.setValue({ control: '' }) - - await flushPromises() - - expect(group.invalid.value).toEqual(false) - expect(group.hasError('test')).toEqual(false) - expect(group.hasError('required', 'control')).toEqual(false) - expect(group.hasError('required', 'group.control')).toEqual(false) + expect(group.invalid.value).toEqual(true) + expect(group.hasError('test')).toEqual(true) + expect(group.hasError('required', 'control')).toEqual(true) + expect(group.hasError('required', 'group.control')).toEqual(true) - group.markAsBlurred() + group.setValue({ control: 'test', group: { control: 'test' } }) await flushPromises() expect(group.invalid.value).toEqual(true) expect(group.hasError('test')).toEqual(true) expect(group.hasError('required', 'control')).toEqual(true) - expect(group.hasError('required', 'group.control')).toEqual(false) + expect(group.hasError('required', 'group.control')).toEqual(true) - group.setValue({ control: 'test' }) + group.markAsBlurred() await flushPromises() - expect(group.invalid.value).toEqual(false) + expect(group.invalid.value).toEqual(true) expect(group.hasError('test')).toEqual(false) expect(group.hasError('required', 'control')).toEqual(false) - expect(group.hasError('required', 'group.control')).toEqual(false) + expect(group.hasError('required', 'group.control')).toEqual(true) await group.validate() - expect(group.invalid.value).toEqual(true) - expect(group.hasError('required', 'group.control')).toEqual(true) + expect(group.invalid.value).toEqual(false) + expect(group.hasError('required', 'group.control')).toEqual(false) }) test('submit trigger validate work', async () => { @@ -242,23 +252,24 @@ describe('formGroup.ts', () => { { trigger: 'submit', validators: _validator }, ) - group.setValue({ control: '' }) - group.markAsBlurred() + expect(group.invalid.value).toEqual(true) + expect(group.hasError('test')).toEqual(true) + expect(group.hasError('required', 'control')).toEqual(true) + expect(group.hasError('async', 'group.control')).toEqual(false) + await flushPromises() - expect(group.invalid.value).toEqual(false) - expect(group.hasError('test')).toEqual(false) - expect(group.hasError('required', 'control')).toEqual(false) - expect(group.hasError('async', 'group.control')).toEqual(false) + expect(group.hasError('async', 'group.control')).toEqual(true) - await group.validate() + group.setValue({ control: 'test' }) + await flushPromises() expect(group.invalid.value).toEqual(true) expect(group.hasError('test')).toEqual(true) expect(group.hasError('required', 'control')).toEqual(true) expect(group.hasError('async', 'group.control')).toEqual(true) - group.setValue({ control: 'test' }) + group.markAsBlurred() await flushPromises() expect(group.invalid.value).toEqual(true) diff --git a/packages/cdk/forms/__tests__/useForms.spec.ts b/packages/cdk/forms/__tests__/useForms.spec.ts index 99f892b8e..72ad56ea5 100644 --- a/packages/cdk/forms/__tests__/useForms.spec.ts +++ b/packages/cdk/forms/__tests__/useForms.spec.ts @@ -4,7 +4,7 @@ import { Validators } from '../src/validators' interface BasicGroup { control1: string - control2: number + control2: string array: string[] group: { control: string @@ -25,21 +25,23 @@ describe('useForms.ts', () => { }) expect(group.getValue()).toEqual(basicValue) - expect(group.invalid.value).toEqual(false) - expect(group.hasError('required', 'control1')).toEqual(false) - expect(group.hasError('required', 'control2')).toEqual(false) + expect(group.invalid.value).toEqual(true) + expect(group.hasError('required', 'control1')).toEqual(true) + expect(group.hasError('required', 'control2')).toEqual(true) - group.setValue({ control1: '' }) + group.setValue({ control1: 'test', control2: 'test' }) await flushPromises() - expect(group.getValue()).toEqual({ ...basicValue, control1: '' }) + expect(group.getValue()).toEqual({ ...basicValue, control1: 'test', control2: 'test' }) expect(group.invalid.value).toEqual(true) - expect(group.hasError('required', 'control1')).toEqual(true) - expect(group.hasError('required', 'control2')).toEqual(false) + expect(group.hasError('required', 'control1')).toEqual(false) + expect(group.hasError('required', 'control2')).toEqual(true) group.markAsBlurred() await flushPromises() - expect(group.hasError('required', 'control2')).toEqual(true) + expect(group.invalid.value).toEqual(false) + expect(group.hasError('required', 'control1')).toEqual(false) + expect(group.hasError('required', 'control2')).toEqual(false) }) }) diff --git a/packages/cdk/forms/src/controls/abstractControl.ts b/packages/cdk/forms/src/controls/abstractControl.ts index 29dacccec..5f1f05df4 100644 --- a/packages/cdk/forms/src/controls/abstractControl.ts +++ b/packages/cdk/forms/src/controls/abstractControl.ts @@ -24,7 +24,7 @@ export abstract class AbstractControl { /** * The ref value for the control. */ - readonly valueRef!: any + readonly valueRef!: DeepReadonly> /** * The validation status of the control, there are three possible validation status values: @@ -32,37 +32,52 @@ export abstract class AbstractControl { * * **invalid**: This control has failed at least one validation check. * * **validating**: This control is in the midst of conducting a validation check. */ - readonly status: DeepReadonly> + readonly status!: DeepReadonly> /** * An object containing any errors generated by failing validation, or null if there are no errors. */ - readonly errors: DeepReadonly> + readonly errors!: DeepReadonly> /** * A control is valid when its `status` is `valid`. */ - readonly valid: ComputedRef + readonly valid!: ComputedRef /** * A control is invalid when its `status` is `invalid`. */ - readonly invalid: ComputedRef + readonly invalid!: ComputedRef /** * A control is validating when its `status` is `validating`. */ - readonly validating: ComputedRef + readonly validating!: ComputedRef /** * A control is marked `blurred` once the user has triggered a `blur` event on it. */ - readonly blurred: ComputedRef + readonly blurred!: ComputedRef /** * A control is `unblurred` if the user has not yet triggered a `blur` event on it. */ - readonly unblurred: ComputedRef + readonly unblurred!: ComputedRef + + /** + * A control is `dirty` if the user has changed the value in the UI. + */ + readonly dirty!: ComputedRef + + /** + * A control is `pristine` if the user has not yet changed the value in the UI. + */ + readonly pristine!: ComputedRef + + /** + * A collection of child controls. + */ + readonly controls: Partial> | AbstractControl[] | null = null /** * The parent control. @@ -94,13 +109,14 @@ export abstract class AbstractControl { return this._trigger ?? this._parent?.trigger ?? 'change' } - protected _status = ref('valid') - protected _errors = ref(null) + protected _valueRef!: Ref + protected _status!: Ref + protected _errors!: Ref protected _blurred = ref(false) + protected _dirty = ref(false) - protected _validators: ValidatorFn | null = null - protected _asyncValidators: AsyncValidatorFn | null = null - + private _validators: ValidatorFn | null = null + private _asyncValidators: AsyncValidatorFn | null = null private _parent: FormGroup | FormArray | null = null private _trigger?: TriggerType @@ -108,14 +124,6 @@ export abstract class AbstractControl { validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, ) { - this.status = readonly(this._status) - this.errors = readonly(this._errors) - this.valid = computed(() => this.status.value === 'valid') - this.invalid = computed(() => this.status.value === 'invalid') - this.validating = computed(() => this.status.value === 'validating') - this.blurred = computed(() => this._blurred.value) - this.unblurred = computed(() => !this._blurred.value) - this._convertOptions(validatorOrOptions, asyncValidator) } @@ -146,6 +154,16 @@ export abstract class AbstractControl { */ abstract markAsUnblurred(): void + /** + * Marks the control as `dirty`. + */ + abstract markAsDirty(): void + + /** + * Marks the control as `pristine`. + */ + abstract markAsPristine(): void + /** * Running validations manually, rather than automatically. */ @@ -217,7 +235,7 @@ export abstract class AbstractControl { */ getError(errorCode: keyof ErrorMessages, path?: Array | string): ValidationError | null { const control = path ? this.get(path) : this - return control?.errors?.value?.[errorCode] || null + return control?._errors?.value?.[errorCode] || null } /** @@ -245,8 +263,8 @@ export abstract class AbstractControl { * @param cb The callback when the value changes * @param options Optional options of watch, the default value of `deep` is `true` */ - watchValue(cb: WatchCallback, options?: WatchOptions): WatchStopHandle { - return watch(this.valueRef, cb, { deep: true, ...options }) + watchValue(cb: WatchCallback, options?: WatchOptions): WatchStopHandle { + return watch(this._valueRef, cb, { deep: true, ...options }) } /** @@ -259,29 +277,68 @@ export abstract class AbstractControl { cb: WatchCallback, options?: WatchOptions, ): WatchStopHandle { - return watch(this.status, cb, options) + return watch(this._status, cb, options) } protected async _validate(): Promise { - const value = this.getValue() - let newErrors = this._runValidator(value) - if (isNil(newErrors)) { - newErrors = await this._runAsyncValidator(value) + let newErrors = null + let value = null + if (this._validators) { + value = this.getValue() + newErrors = this._validators(value, this) + } + if (isNil(newErrors) && this._asyncValidators) { + value = this._validators ? value : this.getValue() + this._status.value = 'validating' + newErrors = await this._asyncValidators(value, this) } this.setErrors(newErrors) return newErrors } - private _runValidator(value: any): ValidationErrors | null { - return this._validators ? this._validators(value, this) : null + protected _initAllStatus() { + ;(this as any).valueRef = readonly(this._valueRef) + this._initErrorsAndStatus() + ;(this as any).status = readonly(this._status) + ;(this as any).errors = readonly(this._errors) + ;(this as any).valid = computed(() => this._status.value === 'valid') + ;(this as any).invalid = computed(() => this._status.value === 'invalid') + ;(this as any).validating = computed(() => this._status.value === 'validating') + ;(this as any).blurred = computed(() => this._blurred.value) + ;(this as any).unblurred = computed(() => !this._blurred.value) + ;(this as any).dirty = computed(() => this._dirty.value) + ;(this as any).pristine = computed(() => !this._dirty.value) } - private _runAsyncValidator(value: any): Promise { - if (!this._asyncValidators) { - return Promise.resolve(null) + private _initErrorsAndStatus() { + let errors = null + let value = null + if (this._validators) { + value = this.getValue() + errors = this._validators(value, this) + } + + let status: ValidationStatus = errors ? 'invalid' : 'valid' + if (status === 'valid' && this.controls) { + for (const key in this.controls) { + const controlStatus = (this.controls as any)[key].status.value + if (controlStatus === 'invalid') { + status = 'invalid' + break + } + } + } + + this._errors = ref(errors) + this._status = ref(status) + if (status === 'valid' && this._asyncValidators) { + value = this._validators ? value : this.getValue() + this._status.value = 'validating' + this._asyncValidators(value, this).then(asyncErrors => { + this._errors.value = asyncErrors + this._status.value = asyncErrors ? 'invalid' : 'valid' + }) } - this._status.value = 'validating' - return this._asyncValidators(value, this) } private _convertOptions( diff --git a/packages/cdk/forms/src/controls/formArray.ts b/packages/cdk/forms/src/controls/formArray.ts index 4c0d8f55c..a38cdbf7b 100644 --- a/packages/cdk/forms/src/controls/formArray.ts +++ b/packages/cdk/forms/src/controls/formArray.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Ref, UnwrapRef, WatchStopHandle } from 'vue' +import type { DeepReadonly, Ref, UnwrapRef, WatchStopHandle } from 'vue' import type { AsyncValidatorFn, ValidatorFn, ValidatorOptions, ValidationErrors, ValidationStatus } from '../types' import { ref, watch, watchEffect } from 'vue' @@ -10,7 +10,7 @@ export class FormArray extends AbstractControl { /** * The ref value for the form array. */ - readonly valueRef: Ref>> + readonly valueRef!: DeepReadonly>>> /** * Length of the control array. @@ -19,21 +19,30 @@ export class FormArray extends AbstractControl { return this.controls.length } + protected _valueRef!: Ref>>> + private _statusWatchStopHandle: WatchStopHandle | null = null private _blurredWatchStopHandle: WatchStopHandle | null = null + private _dirtyWatchStopHandle: WatchStopHandle | null = null constructor( - public controls: AbstractControl[], + /** + * An array of child controls. Each child control is given an index where it is registered. + */ + public readonly controls: AbstractControl[], validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, ) { super(validatorOrOptions, asyncValidator) controls.forEach(control => control.setParent(this as any)) - this.valueRef = ref(this._calculateValue(controls)) + this._valueRef = ref(this._calculateValue()) as Ref>>> + + this._initAllStatus() this._watchValid() this._watchStatus() this._watchBlurred() + this._watchDirty() } /** @@ -53,9 +62,7 @@ export class FormArray extends AbstractControl { push(control: AbstractControl): void { this.controls.push(control) this._registerControl(control) - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -67,9 +74,7 @@ export class FormArray extends AbstractControl { insert(index: number, control: AbstractControl): void { this.controls.splice(index, 0, control) this._registerControl(control) - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -79,9 +84,7 @@ export class FormArray extends AbstractControl { */ removeAt(index: number): void { this.controls.splice(index, 1) - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -93,9 +96,7 @@ export class FormArray extends AbstractControl { setControl(index: number, control: AbstractControl): void { this.controls.splice(index, 1, control) this._registerControl(control) - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -139,6 +140,20 @@ export class FormArray extends AbstractControl { this.controls.forEach(control => control.markAsUnblurred()) } + /** + * Marks all controls of the form array as `dirty`. + */ + markAsDirty(): void { + this.controls.forEach(control => control.markAsDirty()) + } + + /** + * Marks all controls of the form array as `pristine`. + */ + markAsPristine(): void { + this.controls.forEach(control => control.markAsPristine()) + } + /** * Running validations manually, rather than automatically. */ @@ -149,9 +164,9 @@ export class FormArray extends AbstractControl { private _watchValid() { watch( - [this.valueRef, this.blurred], - () => { - if (this.trigger === 'change' || (this.trigger === 'blur' && this.blurred.value)) { + [this._valueRef, this._blurred], + ([_, blurred]) => { + if (this.trigger === 'change' || (this.trigger === 'blur' && blurred)) { this._validate() } }, @@ -164,7 +179,7 @@ export class FormArray extends AbstractControl { this._statusWatchStopHandle() } this._statusWatchStopHandle = watchEffect(() => { - let status: ValidationStatus = this.errors.value ? 'invalid' : 'valid' + let status: ValidationStatus = this._errors.value ? 'invalid' : 'valid' if (status === 'valid') { for (const control of this.controls) { const controlStatus = control.status.value @@ -196,8 +211,31 @@ export class FormArray extends AbstractControl { }) } - private _calculateValue(controls: AbstractControl[]) { - return controls.map(control => control.valueRef) + private _watchDirty() { + if (this._dirtyWatchStopHandle) { + this._dirtyWatchStopHandle() + } + this._dirtyWatchStopHandle = watchEffect(() => { + let dirty = false + for (const control of this.controls) { + if (control.dirty.value) { + dirty = true + break + } + } + this._dirty.value = dirty + }) + } + + private _calculateValue() { + return this.controls.map(control => control.valueRef) as Array>> + } + + private _refreshValueAndWatch() { + this._valueRef.value = this._calculateValue() + this._watchStatus() + this._watchBlurred() + this._watchDirty() } private _registerControl(control: AbstractControl) { diff --git a/packages/cdk/forms/src/controls/formControl.ts b/packages/cdk/forms/src/controls/formControl.ts index e054a5c86..c1db3f059 100644 --- a/packages/cdk/forms/src/controls/formControl.ts +++ b/packages/cdk/forms/src/controls/formControl.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Ref, UnwrapRef } from 'vue' +import type { DeepReadonly, Ref, UnwrapRef } from 'vue' import type { AsyncValidatorFn, ValidatorFn, ValidatorOptions, ValidationErrors } from '../types' import { ref, watch } from 'vue' @@ -9,43 +9,52 @@ export class FormControl extends AbstractControl { /** * The ref value for the control. */ - readonly valueRef: Ref | null> + readonly valueRef!: DeepReadonly | null>> + + protected _valueRef: Ref | null> private _initValue: T | null = null + constructor( initValue: T | null = null, validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, ) { super(validatorOrOptions, asyncValidator) - this.valueRef = ref(initValue) + this._valueRef = ref(initValue) this._initValue = initValue - this._watchEffect() + this._initAllStatus() + + this._watchValid() + this._watchStatus() } /** - * Resets the form control, marking it `unblurred`, and setting the value to initialization value. + * Resets the form control, marking it `unblurred`, `pristine`, + * and setting the value to initialization value. */ reset(): void { - this.valueRef.value = this._initValue as any + this._valueRef.value = this._initValue as any this.markAsUnblurred() + this.markAsPristine() } /** - * Sets a new value for the form control. + * Sets a new value for the form control, marking it `dirty`. * * @param value The new value. */ setValue(value: T | null): void { - this.valueRef.value = value as any + this._valueRef.value = value as any + this.markAsDirty() } /** * The aggregate value of the form control. */ getValue(): T | null { - return this.valueRef.value as T + return this._valueRef.value as T } /** @@ -62,6 +71,20 @@ export class FormControl extends AbstractControl { this._blurred.value = false } + /** + * Marks the control as `dirty`. + */ + markAsDirty(): void { + this._dirty.value = true + } + + /** + * Marks the control as `pristine`. + */ + markAsPristine(): void { + this._dirty.value = false + } + /** * Running validations manually, rather than automatically. */ @@ -69,14 +92,16 @@ export class FormControl extends AbstractControl { return this._validate() } - private _watchEffect() { - watch([this.valueRef, this.blurred], () => { - if (this.trigger === 'change' || (this.trigger === 'blur' && this.blurred.value)) { + private _watchValid() { + watch([this._valueRef, this._blurred], ([_, blurred]) => { + if (this.trigger === 'change' || (this.trigger === 'blur' && blurred)) { this._validate() } }) + } - watch(this.errors, errors => { + private _watchStatus() { + watch(this._errors, errors => { this._status.value = errors ? 'invalid' : 'valid' }) } diff --git a/packages/cdk/forms/src/controls/formGroup.ts b/packages/cdk/forms/src/controls/formGroup.ts index 5666439e8..edb42c1e1 100644 --- a/packages/cdk/forms/src/controls/formGroup.ts +++ b/packages/cdk/forms/src/controls/formGroup.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { Ref, UnwrapRef, WatchStopHandle } from 'vue' +import type { DeepReadonly, Ref, UnwrapRef, WatchStopHandle } from 'vue' import type { AsyncValidatorFn, ValidatorFn, ValidatorOptions, ValidationErrors, ValidationStatus } from '../types' import { ref, watch, watchEffect } from 'vue' @@ -12,23 +12,32 @@ export class FormGroup> extends AbstractControl { /** * The ref value for the form group. */ - readonly valueRef: Ref>>>> + readonly valueRef!: DeepReadonly>>>>>> + + protected _valueRef!: Ref>>>>> private _statusWatchStopHandle: WatchStopHandle | null = null private _blurredWatchStopHandle: WatchStopHandle | null = null + private _dirtyWatchStopHandle: WatchStopHandle | null = null constructor( - public controls: Partial>, + /** + * A collection of child controls. The key for each child is the name under which it is registered. + */ + public readonly controls: Partial>, validatorOrOptions?: ValidatorFn | ValidatorFn[] | ValidatorOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null, ) { super(validatorOrOptions, asyncValidator) this._forEachChild(control => control.setParent(this as any)) - this.valueRef = ref(this._calculateValue(controls)) as Ref>>>> + this._valueRef = ref(this._calculateValue()) as Ref>>>>> + + this._initAllStatus() this._watchValid() this._watchStatus() this._watchBlurred() + this._watchDirty() } /** @@ -39,9 +48,7 @@ export class FormGroup> extends AbstractControl { */ addControl(name: keyof T, control: AbstractControl): void { this._registerControl(name, control) - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -51,9 +58,7 @@ export class FormGroup> extends AbstractControl { */ removeControl(name: keyof T): void { delete this.controls[name] - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -65,9 +70,7 @@ export class FormGroup> extends AbstractControl { setControl(name: keyof T, control: AbstractControl): void { delete this.controls[name] this._registerControl(name, control) - this.valueRef.value = this._calculateValue(this.controls) - this._watchStatus() - this._watchBlurred() + this._refreshValueAndWatch() } /** @@ -113,6 +116,20 @@ export class FormGroup> extends AbstractControl { this._forEachChild(control => control.markAsUnblurred()) } + /** + * Marks all controls of the form group as `dirty`. + */ + markAsDirty(): void { + this._forEachChild(control => control.markAsDirty()) + } + + /** + * Marks all controls of the form group as `pristine`. + */ + markAsPristine(): void { + this._forEachChild(control => control.markAsPristine()) + } + /** * Running validations manually, rather than automatically. */ @@ -123,9 +140,9 @@ export class FormGroup> extends AbstractControl { private _watchValid() { watch( - [this.valueRef, this.blurred], - () => { - if (this.trigger === 'change' || (this.trigger === 'blur' && this.blurred.value)) { + [this._valueRef, this._blurred], + ([_, blurred]) => { + if (this.trigger === 'change' || (this.trigger === 'blur' && blurred)) { this._validate() } }, @@ -138,7 +155,7 @@ export class FormGroup> extends AbstractControl { this._statusWatchStopHandle() } this._statusWatchStopHandle = watchEffect(() => { - let status: ValidationStatus = this.errors.value ? 'invalid' : 'valid' + let status: ValidationStatus = this._errors.value ? 'invalid' : 'valid' if (status === 'valid') { for (const key in this.controls) { const controlStatus = this.controls[key]!.status.value @@ -170,16 +187,39 @@ export class FormGroup> extends AbstractControl { }) } - private _calculateValue(controls: Partial>>) { - const value = {} as Partial>>> + private _watchDirty() { + if (this._dirtyWatchStopHandle) { + this._dirtyWatchStopHandle() + } + this._dirtyWatchStopHandle = watchEffect(() => { + let dirty = false + for (const key in this.controls) { + if (this.controls[key]!.dirty.value) { + dirty = true + break + } + } + this._dirty.value = dirty + }) + } + + private _calculateValue() { + const value = {} as Partial>>>> - Object.keys(controls).forEach(key => { - value[key as keyof T] = controls[key as keyof T]!.valueRef + Object.keys(this.controls).forEach(key => { + value[key as keyof T] = this.controls[key as keyof T]!.valueRef }) return value } + private _refreshValueAndWatch() { + this._valueRef.value = this._calculateValue() + this._watchStatus() + this._watchBlurred() + this._watchDirty() + } + private _registerControl(name: keyof T, control: AbstractControl) { if (hasOwnProperty(this.controls, name as string)) { return diff --git a/packages/cdk/forms/src/typeof.ts b/packages/cdk/forms/src/typeof.ts index 085fa02bb..411b22fcc 100644 --- a/packages/cdk/forms/src/typeof.ts +++ b/packages/cdk/forms/src/typeof.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { FormArray } from './controls/formArray' -import type { FormControl } from './controls/formControl' import type { FormGroup } from './controls/formGroup' +import type { FormControl } from './controls/formControl' import { hasOwnProperty, isArray } from '@idux/cdk/utils' import { AbstractControl } from './controls/abstractControl' @@ -17,7 +17,7 @@ export const isFormArray = (val: unknown): val is FormArray => { // Since AbstractControl be dependent on the function, `val instanceof FormGroup` cannot be used here. export const isFormGroup = (val: unknown): val is FormGroup => { - return isAbstractControl(val) && hasOwnProperty(val, 'controls') && !isArray((val as FormGroup).controls) + return isAbstractControl(val) && !isFormControl(val) && !isFormArray(val) } export const isFormControl = (val: unknown): val is FormControl => {