From e64574014be2b7bb25a36d945a2975e10fab9c60 Mon Sep 17 00:00:00 2001 From: danranvm Date: Wed, 13 Jul 2022 16:23:24 +0800 Subject: [PATCH] feat(cdk:forms): add useuseAccessorAndControl, useAccessor, useControl --- packages/cdk/forms/__tests__/utils.spec.ts | 16 +- packages/cdk/forms/demo/CustomForm.md | 4 +- packages/cdk/forms/demo/CustomForm.vue | 10 +- packages/cdk/forms/demo/CustomInput.md | 4 +- packages/cdk/forms/demo/CustomInput.vue | 20 +- packages/cdk/forms/docs/Index.zh.md | 64 ++-- packages/cdk/forms/docs/Overview.zh.md | 315 ++---------------- packages/cdk/forms/src/controls.ts | 43 ++- packages/cdk/forms/src/utils.ts | 174 +++++++++- packages/cdk/utils/src/composable.ts | 10 +- packages/components/cascader/src/Cascader.tsx | 10 +- .../src/composables/useSelectedState.ts | 8 +- packages/components/cascader/src/token.ts | 4 +- packages/components/checkbox/src/Checkbox.tsx | 20 +- .../components/checkbox/src/CheckboxGroup.tsx | 6 +- packages/components/checkbox/src/token.ts | 4 +- .../components/date-picker/src/DatePicker.tsx | 10 +- .../date-picker/src/DateRangePicker.tsx | 4 +- .../date-picker/src/composables/useControl.ts | 11 +- .../src/composables/useInputProps.ts | 2 +- .../src/composables/useInputState.ts | 54 +-- .../src/composables/useOverlayProps.ts | 2 +- .../src/composables/usePickerState.ts | 38 +-- .../src/composables/useRangeControl.ts | 11 +- .../src/composables/useTriggerProps.ts | 10 +- packages/components/date-picker/src/types.ts | 4 +- packages/components/form/demo/Basic.vue | 95 ++---- packages/components/form/docs/Index.zh.md | 78 ++--- packages/components/form/src/Form.tsx | 4 +- packages/components/form/src/FormWrapper.tsx | 4 +- .../components/form/src/composables/public.ts | 20 +- .../form/src/composables/useFormItem.ts | 4 +- .../input-number/src/useInputNumber.ts | 28 +- packages/components/input/src/Input.tsx | 2 +- packages/components/input/src/useInput.ts | 27 +- packages/components/radio/demo/Group.vue | 2 +- packages/components/radio/src/Radio.tsx | 20 +- packages/components/radio/src/RadioGroup.tsx | 6 +- packages/components/radio/src/token.ts | 4 +- packages/components/rate/src/Rate.tsx | 18 +- packages/components/select/src/Select.tsx | 15 +- .../src/composables/useSelectedState.ts | 13 +- packages/components/slider/src/useSlider.ts | 24 +- packages/components/switch/demo/Confirm.vue | 19 +- packages/components/switch/src/Switch.tsx | 18 +- packages/components/switch/src/types.ts | 27 +- packages/components/textarea/src/Textarea.tsx | 20 +- .../components/textarea/src/useAutoRows.ts | 13 +- .../components/time-picker/src/TimePicker.tsx | 4 +- .../time-picker/src/TimeRangePicker.tsx | 4 +- .../src/composables/useInputProps.ts | 2 +- .../src/composables/useOverlayProps.ts | 2 +- .../src/composables/usePickerState.ts | 40 +-- .../src/composables/useTriggerProps.ts | 7 +- .../components/tree-select/src/TreeSelect.tsx | 11 +- .../src/composables/useSelectedState.ts | 18 +- packages/components/tree-select/src/token.ts | 4 +- 57 files changed, 629 insertions(+), 782 deletions(-) diff --git a/packages/cdk/forms/__tests__/utils.spec.ts b/packages/cdk/forms/__tests__/utils.spec.ts index 687df0d81..b49efadfe 100644 --- a/packages/cdk/forms/__tests__/utils.spec.ts +++ b/packages/cdk/forms/__tests__/utils.spec.ts @@ -1,22 +1,20 @@ import { DOMWrapper, flushPromises, mount } from '@vue/test-utils' -import { computed, provide } from 'vue' +import { provide } from 'vue' import { Validators } from '..' import { FormGroup } from '../src/controls' import { useFormGroup } from '../src/useForms' -import { FORMS_CONTROL_TOKEN, useValueAccessor, useValueControl } from '../src/utils' +import { FORMS_CONTROL_TOKEN, useAccessorAndControl, useControl } from '../src/utils' const InputComponent = { - template: ``, - props: ['value', 'control'], + template: ``, + props: ['value', 'control', 'disabled'], emits: ['update:value'], setup() { - const control = useValueControl() - const accessor = useValueAccessor({ control }) - const valueRef = computed(() => accessor.valueRef.value) + const { accessor } = useAccessorAndControl() const onInput = (evt: Event) => accessor.setValue((evt.target as HTMLInputElement).value) const onBlur = () => accessor.markAsBlurred() - return { valueRef, onInput, onBlur } + return { accessor, onInput, onBlur } }, } @@ -24,7 +22,7 @@ const FormComponent = { template: `
`, props: ['control'], setup() { - const control = useValueControl() + const control = useControl() provide(FORMS_CONTROL_TOKEN, control) }, } diff --git a/packages/cdk/forms/demo/CustomForm.md b/packages/cdk/forms/demo/CustomForm.md index 5f74dde49..1cb92637a 100644 --- a/packages/cdk/forms/demo/CustomForm.md +++ b/packages/cdk/forms/demo/CustomForm.md @@ -9,10 +9,10 @@ title: 自定以一个支持 `AbstractControl` 的表单组件。 -更多实现细节,请参考:[IxForm](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) 与 [IxFormItem](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/FormItem.tsx)。 +更多实现细节,请参考:[Form](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) 与 [FormItem](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/FormItem.tsx)。 ## en Customize with a form component that supports `AbstractControl`. -For more implementation details, see: [IxForm](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) and [IxFormItem](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/FormItem.tsx). +For more implementation details, see: [Form](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) and [FormItem](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/FormItem.tsx). diff --git a/packages/cdk/forms/demo/CustomForm.vue b/packages/cdk/forms/demo/CustomForm.vue index 6b578a5ef..e34bfad5b 100644 --- a/packages/cdk/forms/demo/CustomForm.vue +++ b/packages/cdk/forms/demo/CustomForm.vue @@ -5,16 +5,16 @@ diff --git a/packages/cdk/forms/demo/CustomInput.md b/packages/cdk/forms/demo/CustomInput.md index f863e1370..8da9515ac 100644 --- a/packages/cdk/forms/demo/CustomInput.md +++ b/packages/cdk/forms/demo/CustomInput.md @@ -9,10 +9,10 @@ title: 自定义一个支持 `AbstractControl` 的输入控件。 -更多实现细节,请参考:[IxInput](https://github.com/IDuxFE/idux/blob/main/packages/components/input/src/Input.tsx) 或其他输入型组件。 +更多实现细节,请参考:[Input](https://github.com/IDuxFE/idux/blob/main/packages/components/input/src/Input.tsx) 或其他输入型组件。 ## en Customize with an input control that supports `AbstractControl`. -For more implementation details, see: [IxForm](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) or other input components. +For more implementation details, see: [Input](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Input.tsx) or other input components. diff --git a/packages/cdk/forms/demo/CustomInput.vue b/packages/cdk/forms/demo/CustomInput.vue index 96e853006..ae2d322f6 100644 --- a/packages/cdk/forms/demo/CustomInput.vue +++ b/packages/cdk/forms/demo/CustomInput.vue @@ -1,30 +1,24 @@ ``` -### 实现支持控制器的表单组件 +### 实现支持控制器的表单容器组件 自定以一个支持 `AbstractControl` 的表单组件。 -更多实现细节,请参考:[IxForm](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) 与 [IxFormItem](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/FormItem.tsx)。 +更多实现细节,请参考:[Form](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/Form.tsx) 与 [FormItem](https://github.com/IDuxFE/idux/blob/main/packages/components/form/src/FormItem.tsx)。 ```html ``` @@ -265,273 +259,12 @@ const formGroup = useFormGroup({ }) ``` -## 类型定义 +## FAQ -### AbstractControl +### 更多的使用示例和场景? -```ts -export declare abstract class AbstractControl { - readonly uid: number; - /** - * A collection of child controls. - */ - readonly controls: ComputedRef | AbstractControl>[] | undefined>; - /** - * The ref value for the control. - */ - readonly valueRef: ComputedRef; - /** - * The validation status of the control, there are three possible validation status values: - * * **valid**: This control has passed all validation checks. - * * **invalid**: This control has failed at least one validation check. - * * **validating**: This control is in the midst of conducting a validation check. - */ - readonly status: ComputedRef; - /** - * An object containing any errors generated by failing validation, or undefined if there are no errors. - */ - readonly errors: ComputedRef; - /** - * A control is valid when its `status` is `valid`. - */ - readonly valid: ComputedRef; - /** - * A control is invalid when its `status` is `invalid`. - */ - readonly invalid: ComputedRef; - /** - * A control is validating when its `status` is `validating`. - */ - readonly validating: ComputedRef; - /** - * A control is validating when its `status` is `disabled`. - */ - readonly disabled: ComputedRef; - /** - * A control is marked `blurred` once the user has triggered a `blur` event on it. - */ - readonly blurred: ComputedRef; - /** - * A control is `unblurred` if the user has not yet triggered a `blur` event on it. - */ - 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; - /** - * The parent control. - */ - get parent(): AbstractControl | undefined; - /** - * Retrieves the top-level ancestor of this control. - */ - get root(): AbstractControl; - /** - * Reports the trigger validate of the `AbstractControl`. - * Possible values: `'change'` | `'blur'` | `'submit'` - * Default value: `'change'` - */ - get trigger(): TriggerType; - /** - * Sets a new value for the control. - * - * @param value The new value. - * @param options - * * `dirty`: Marks it dirty, default is false. - */ - abstract setValue(value: T | Partial, options?: { dirty?: boolean }): void; - /** - * The aggregate value of the control. - * - * @param options - * * `skipDisabled`: Ignore value of disabled control, default is false. - */ - abstract getValue(options?: { skipDisabled?: boolean }): T; - /** - * Resets the control, marking it `unblurred` `pristine`, and setting the value to initialization value. - */ - reset(): void; - /** - * Running validations manually, rather than automatically. - */ - validate(): Promise; - /** - * Marks the control as `disable`. - */ - disable(): void; - /** - * Enables the control, - */ - enable(): void; - /** - * Marks the control as `blurred`. - */ - markAsBlurred(): void; - /** - * Marks the control as `unblurred`. - */ - markAsUnblurred(): void; - /** - * Marks the control as `dirty`. - */ - markAsDirty(): void; - /** - * Marks the control as `pristine`. - */ - markAsPristine(): void; - /** - * Sets the new sync validator for the form control, it overwrites existing sync validators. - * If you want to clear all sync validators, you can pass in a undefined. - */ - setValidator(newValidator?: ValidatorFn | ValidatorFn[]): void; - /** - * Sets the new async validator for the form control, it overwrites existing async validators. - * If you want to clear all async validators, you can pass in a undefined. - */ - setAsyncValidator(newAsyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | undefined): void; - /** - * Retrieves a child control given the control's name or path. - * - * @param path A dot-delimited string or array of string/number values that define the path to the - * control. - */ - get>(path: K): AbstractControl | undefined; - get(path: K): AbstractControl; - get(path: ControlPathType): AbstractControl | undefined; - /** - * Sets errors on a form control when running validations manually, rather than automatically. - */ - setErrors(errors?: ValidateErrors): void; - /** - * Reports error data for the control with the given path. - * - * @param errorCode The code of the error to check - * @param path A list of control names that designates how to move from the current control - * to the control that should be queried for errors. - */ - getError(errorCode: string, path?: ControlPathType): ValidateError | undefined; - /** - * Reports whether the control with the given path has the error specified. - * - * @param errorCode The code of the error to check - * @param path A list of control names that designates how to move from the current control - * to the control that should be queried for errors. - * - */ - hasError(errorCode: string, path?: ControlPathType): boolean; - /** - * @param parent Sets the parent of the control - */ - setParent(parent: AbstractControl): void; - /** - * Watch the ref value for the control. - * - * @param cb The callback when the value changes - * @param options Optional options of watch - */ - watchValue(cb: WatchCallback, options?: WatchOptions): WatchStopHandle; - /** - * Watch the status for the control. - * - * @param cb The callback when the status changes - * @param options Optional options of watch - */ - watchStatus(cb: WatchCallback, options?: WatchOptions): WatchStopHandle; -} -``` +参见 [@idux/components/form](https://idux.site/components/form/zh) -### FormGroup +### 更多的使用细节和文档? -```ts -export declare class FormGroup = Record> extends AbstractControl { - /** - * A collection of child controls. The key for each child is the name under which it is registered. - */ - readonly controls: ComputedRef>; - setValue(value: Partial, options?: { dirty?: boolean }): void; - getValue(): T; - /** - * Add a control to this form group. - * - * @param name The control name to add to the collection - * @param control Provides the control for the given name - */ - addControl>(name: K, control: AbstractControl): void; - /** - * Remove a control from this form group. - * - * @param name The control name to remove from the collection - */ - removeControl>(name: K): void; - /** - * Replace an existing control. - * - * @param name The control name to replace in the collection - * @param control Provides the control for the given name - */ - setControl(name: K, control: AbstractControl): void; -} -``` - -### FormArray - -```ts -export declare class FormArray extends AbstractControl { - /** - * An array of child controls. Each child control is given an index where it is registered. - */ - readonly controls: ComputedRef>[]>; - /** - * Length of the control array. - */ - readonly length: ComputedRef; - setValue(value: Partial>[], options?: { dirty?: boolean }): void; - getValue(): T; - /** - * Get the `AbstractControl` at the given `index` in the array. - * - * @param index Index in the array to retrieve the control - */ - at(index: number): AbstractControl>; - /** - * Insert a new `AbstractControl` at the end of the array. - * - * @param control Form control to be inserted - */ - push(control: AbstractControl>): void; - /** - * Insert a new `AbstractControl` at the given `index` in the array. - * - * @param index Index in the array to insert the control - * @param control Form control to be inserted - */ - insert(index: number, control: AbstractControl>): void; - /** - * Remove the control at the given `index` in the array. - * - * @param index Index in the array to remove the control - */ - removeAt(index: number): void; - /** - * Replace an existing control. - * - * @param index Index in the array to replace the control - * @param control The `AbstractControl` control to replace the existing control - */ - setControl(index: number, control: AbstractControl>): void; -} -``` - -### FormControl - -```ts -export declare class FormControl extends AbstractControl { - setValue(value: T, options?: { dirty?: boolean }): void; - getValue(): T; -} -``` +参见 [@angular/forms](https://angular.cn/guide/forms-overview) diff --git a/packages/cdk/forms/src/controls.ts b/packages/cdk/forms/src/controls.ts index 0d09bff2e..5d61b7d5e 100644 --- a/packages/cdk/forms/src/controls.ts +++ b/packages/cdk/forms/src/controls.ts @@ -7,23 +7,32 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { - AsyncValidatorFn, - TriggerType, - ValidateError, - ValidateErrors, - ValidateStatus, - ValidatorFn, - ValidatorOptions, -} from './types' -import type { ComputedRef, Ref, WatchCallback, WatchOptions, WatchStopHandle } from 'vue' - -import { computed, ref, shallowRef, watch, watchEffect } from 'vue' +import { + type ComputedRef, + type Ref, + type WatchCallback, + type WatchOptions, + type WatchStopHandle, + computed, + ref, + shallowRef, + watch, + watchEffect, +} from 'vue' import { isArray, isNil, isPlainObject, isString } from 'lodash-es' import { hasOwnProperty } from '@idux/cdk/utils' +import { + type AsyncValidatorFn, + type TriggerType, + type ValidateError, + type ValidateErrors, + type ValidateStatus, + type ValidatorFn, + type ValidatorOptions, +} from './types' import { Validators } from './validators' type IsNullable = undefined extends T ? K : never @@ -215,7 +224,15 @@ export abstract class AbstractControl { if (this._controls.value) { this._forEachControls(control => control.reset()) } else { - this._valueRef.value = this._calculateInitValue() + const currValue = this._valueRef.value + const initValue = this._calculateInitValue() + if (currValue !== initValue) { + this._valueRef.value = initValue + } else { + // There are cases where the value does not change but the validator changes, + // so manual validation is required here + this._validate() + } this.markAsUnblurred() this.markAsPristine() } diff --git a/packages/cdk/forms/src/utils.ts b/packages/cdk/forms/src/utils.ts index f65870d38..e5c9b2cb4 100644 --- a/packages/cdk/forms/src/utils.ts +++ b/packages/cdk/forms/src/utils.ts @@ -7,28 +7,179 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { AbstractControl, ControlPathType } from './controls' -import type { ComputedRef, InjectionKey, PropType, ShallowRef, WatchStopHandle } from 'vue' - -import { computed, getCurrentInstance, inject, ref, shallowReactive, shallowRef, toRaw, watch } from 'vue' +import { + type ComputedRef, + type InjectionKey, + type ShallowRef, + type WatchStopHandle, + computed, + getCurrentInstance, + inject, + onScopeDispose, + reactive, + shallowReactive, + shallowRef, + toRaw, + watch, + watchEffect, +} from 'vue' import { isNil } from 'lodash-es' import { Logger, NoopFunction, callEmit } from '@idux/cdk/utils' +import { type AbstractControl, type ControlPathType } from './controls' import { isAbstractControl } from './typeof' -/** - * @deprecated - */ -export const controlPropDef = [String, Number, Object] as PropType - export const FORMS_CONTROL_TOKEN: InjectionKey> = Symbol('cdk-forms-control') +export function useControl(controlKey = 'control'): ShallowRef | undefined> { + const { props } = getCurrentInstance()! + const parentControl = inject(FORMS_CONTROL_TOKEN, shallowRef()) + + const control = shallowRef() + let watchStop: WatchStopHandle | undefined + const cleanWatch = () => { + if (watchStop) { + watchStop() + watchStop = undefined + } + } + onScopeDispose(cleanWatch) + + watch( + [() => props[controlKey], parentControl], + ([controlOrPath, pControl]) => { + cleanWatch() + if (isAbstractControl(controlOrPath)) { + control.value = controlOrPath + } else if (!!pControl && !isNil(controlOrPath)) { + watchStop = watch( + pControl.controls, + () => { + const _control = pControl.get(controlOrPath as ControlPathType) + if (__DEV__ && !_control) { + Logger.warn('cdk/forms', `not find control by [${controlOrPath}]`) + } + control.value = _control + }, + { immediate: true }, + ) + } + }, + { immediate: true }, + ) + + return control +} + +export interface FormAccessor { + value: T + disabled: boolean + markAsBlurred: () => void + setValue: (value: T) => void +} + +export function useAccessor( + control: ShallowRef | undefined>, + valueKey = 'value', + disabledKey = 'disabled', +): FormAccessor { + const { props } = getCurrentInstance()! + const accessor = reactive({} as FormAccessor) + let watchStop: WatchStopHandle | undefined + let tempValueWatchStop: WatchStopHandle | undefined + const cleanWatch = () => { + if (watchStop) { + watchStop() + watchStop = undefined + } + if (tempValueWatchStop) { + tempValueWatchStop() + tempValueWatchStop = undefined + } + } + onScopeDispose(cleanWatch) + + watch( + control, + currControl => { + cleanWatch() + + if (currControl) { + watchStop = watchEffect(() => { + accessor.value = currControl.valueRef.value + accessor.disabled = currControl.disabled.value + }) + accessor.setValue = value => currControl.setValue(value, { dirty: true }) + accessor.markAsBlurred = () => currControl.markAsBlurred() + } else { + const tempRef = shallowRef(props[valueKey]) + tempValueWatchStop = watch( + () => props[valueKey], + value => (tempRef.value = value), + ) + watchStop = watchEffect(() => { + accessor.value = props[valueKey] ?? tempRef.value + accessor.disabled = props[disabledKey] as boolean + }) + accessor.setValue = value => { + if (value != toRaw(accessor.value)) { + tempRef.value = value + callEmit((props as any)[`onUpdate:${valueKey}`], value) + } + } + accessor.markAsBlurred = NoopFunction + } + }, + { immediate: true }, + ) + + return accessor +} + +export interface FormAccessorOptions { + /** + * the control key in props + * + * @default 'control' + */ + controlKey?: string + /** + * the value key in props + * + * @default 'value' + */ + valueKey?: string + /** + * the disabled key in props + * + * @default 'disabled' + */ + disabledKey?: string +} + +export function useAccessorAndControl( + options?: FormAccessorOptions, +): { + accessor: FormAccessor + control: ShallowRef | undefined> +} { + const { controlKey, valueKey, disabledKey } = options ?? {} + + const control = useControl(controlKey) + const accessor = useAccessor(control, valueKey, disabledKey) + + return { accessor, control } +} + export interface ValueControlOptions { controlKey?: string } +/** + * @deprecated please use `useControl` or `useAccessorAndControl` instead + */ export function useValueControl( options: ValueControlOptions = {}, ): ShallowRef | undefined> { @@ -81,6 +232,9 @@ export interface ValueAccessor { setValue: (value: T) => void } +/** + * @deprecated please use `useAccessor` or `useAccessorAndControl` instead + */ export function useValueAccessor(options: ValueAccessorOptions): ValueAccessor { const { control, valueKey = 'value', disabledKey = 'disabled' } = options const { props } = getCurrentInstance()! @@ -102,7 +256,7 @@ export function useValueAccessor(options: ValueAccessorOptions): ValueA accessor.setValue = value => currControl.setValue(value, { dirty: true }) accessor.markAsBlurred = () => currControl.markAsBlurred() } else { - const tempRef = ref(props[valueKey]) + const tempRef = shallowRef(props[valueKey]) watchStop = watch( () => props[valueKey], value => (tempRef.value = value), diff --git a/packages/cdk/utils/src/composable.ts b/packages/cdk/utils/src/composable.ts index 1884e8267..dcab1cbfa 100644 --- a/packages/cdk/utils/src/composable.ts +++ b/packages/cdk/utils/src/composable.ts @@ -8,7 +8,6 @@ import { type ComputedRef, type EffectScope, - type Ref, computed, effectScope, onScopeDispose, @@ -71,22 +70,21 @@ export function useControlledProp( key: K, defaultOrFactory?: T[K] | (() => T[K]), ): [ComputedRef, (value: T[K]) => void] { - const tempProp = ref(props[key]) as Ref + const defaultValue = props[key] ?? (isFunction(defaultOrFactory) ? defaultOrFactory() : defaultOrFactory) + const tempProp = shallowRef(defaultValue) watch( () => props[key], value => (tempProp.value = value), ) - const state = computed( - () => props[key] ?? tempProp.value ?? (isFunction(defaultOrFactory) ? defaultOrFactory() : defaultOrFactory)!, - ) + const state = computed(() => props[key] ?? tempProp.value!) const setState = (value: T[K]) => { if (value !== toRaw(state.value)) { tempProp.value = value // eslint-disable-next-line @typescript-eslint/no-explicit-any - callEmit((props as any)[`onUpdate:${key}`], value) + callEmit((props as any)[`onUpdate:${key as string}`], value) } } diff --git a/packages/components/cascader/src/Cascader.tsx b/packages/components/cascader/src/Cascader.tsx index e44be8974..0c5c90d92 100644 --- a/packages/components/cascader/src/Cascader.tsx +++ b/packages/components/cascader/src/Cascader.tsx @@ -7,11 +7,12 @@ import { computed, defineComponent, normalizeClass, provide, ref, watch } from 'vue' +import { useAccessorAndControl } from '@idux/cdk/forms' import { type VKey, useState } from '@idux/cdk/utils' import { ɵOverlay } from '@idux/components/_private/overlay' import { ɵSelector, type ɵSelectorInstance } from '@idux/components/_private/selector' import { useGlobalConfig } from '@idux/components/config' -import { useFormAccessor } from '@idux/components/form' +import { useFormItemRegister } from '@idux/components/form' import { ɵUseOverlayState } from '@idux/components/select' import { useGetKey } from '@idux/components/utils' @@ -54,7 +55,8 @@ export default defineComponent({ const { overlayRef, updateOverlay, overlayOpened, setOverlayOpened } = ɵUseOverlayState(props, config, triggerRef) - const accessor = useFormAccessor() + const { accessor, control } = useAccessorAndControl() + useFormItemRegister(control) const { mergedData, mergedDataMap } = useDataSource( props, @@ -143,7 +145,7 @@ export default defineComponent({ clearIcon={props.clearIcon} config={config} dataSource={selectedStateContext.selectedData.value} - disabled={accessor.disabled.value} + disabled={accessor.disabled} maxLabel={props.maxLabel} multiple={props.multiple} opened={overlayOpened.value} @@ -171,7 +173,7 @@ export default defineComponent({ const overlayProps = { class: overlayClasses.value, clickOutside: true, - disabled: accessor.disabled.value || props.readonly, + disabled: accessor.disabled || props.readonly, offset: defaultOffset, placement: 'bottomStart', target: target.value, diff --git a/packages/components/cascader/src/composables/useSelectedState.ts b/packages/components/cascader/src/composables/useSelectedState.ts index e6cbb91d7..13670ca20 100644 --- a/packages/components/cascader/src/composables/useSelectedState.ts +++ b/packages/components/cascader/src/composables/useSelectedState.ts @@ -9,7 +9,7 @@ import { type ComputedRef, computed, toRaw } from 'vue' import { isNil } from 'lodash-es' -import { type ValueAccessor } from '@idux/cdk/forms' +import { type FormAccessor } from '@idux/cdk/forms' import { NoopArray, type VKey, callEmit, convertArray } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' @@ -30,13 +30,13 @@ export interface SelectedStateContext { export function useSelectedState( props: CascaderProps, - accessor: ValueAccessor, + accessor: FormAccessor, mergedDataMap: ComputedRef>, mergedFullPath: ComputedRef, ): SelectedStateContext { const locale = useGlobalConfig('locale') const selectedKeys = computed(() => { - const tempKeys = convertArray(accessor.valueRef.value) + const tempKeys = convertArray(accessor.value) if (!mergedFullPath.value) { return tempKeys } @@ -107,7 +107,7 @@ export function useSelectedState( } } - const oldValue = toRaw(accessor.valueRef.value) + const oldValue = toRaw(accessor.value) if (currValue !== oldValue) { accessor.setValue(currValue) callEmit(props.onChange, currValue, oldValue) diff --git a/packages/components/cascader/src/token.ts b/packages/components/cascader/src/token.ts index 344206c8b..41e5cd887 100644 --- a/packages/components/cascader/src/token.ts +++ b/packages/components/cascader/src/token.ts @@ -13,7 +13,7 @@ import type { ExpandableContext } from './composables/useExpandable' import type { SearchableContext } from './composables/useSearchable' import type { SelectedStateContext } from './composables/useSelectedState' import type { CascaderProps } from './types' -import type { ValueAccessor } from '@idux/cdk/forms' +import type { FormAccessor } from '@idux/cdk/forms' import type { CascaderConfig } from '@idux/components/config' import type { GetKeyFn } from '@idux/components/utils' import type { ComputedRef, InjectionKey, Slots } from 'vue' @@ -34,7 +34,7 @@ export interface CascaderContext mergedExpandIcon: ComputedRef mergedFullPath: ComputedRef mergedLabelKey: ComputedRef - accessor: ValueAccessor + accessor: FormAccessor inputValue: ComputedRef setInputValue: (value: string) => void overlayOpened: ComputedRef diff --git a/packages/components/checkbox/src/Checkbox.tsx b/packages/components/checkbox/src/Checkbox.tsx index 4bdf2b2f3..9d8772fe5 100644 --- a/packages/components/checkbox/src/Checkbox.tsx +++ b/packages/components/checkbox/src/Checkbox.tsx @@ -9,9 +9,10 @@ import { type ComputedRef, computed, defineComponent, inject, normalizeClass, re import { isNil } from 'lodash-es' +import { useAccessorAndControl } from '@idux/cdk/forms' import { callEmit } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' -import { FORM_TOKEN, useFormAccessor, useFormElement } from '@idux/components/form' +import { FORM_TOKEN, useFormElement, useFormItemRegister } from '@idux/components/form' import { convertStringVNode, useKey } from '@idux/components/utils' import { type CheckboxGroupContext, checkboxGroupToken } from './token' @@ -121,13 +122,13 @@ const useCheckbox = ( if (checkboxGroup) { const { props: groupProps, accessor } = checkboxGroup - isChecked = computed(() => (accessor.valueRef.value || []).includes(mergedValue.value)) - isDisabled = computed(() => accessor.disabled.value || !!props.disabled) + isChecked = computed(() => (accessor.value || []).includes(mergedValue.value)) + isDisabled = computed(() => accessor.disabled || !!props.disabled) handleBlur = (evt: FocusEvent) => { isFocused.value = false - callEmit(props.onBlur, evt) accessor.markAsBlurred() + callEmit(props.onBlur, evt) } handleChange = (evt: Event) => { @@ -137,7 +138,7 @@ const useCheckbox = ( const checkValue = checked ? trueValue : falseValue const oldCheckValue = !checked ? trueValue : falseValue - const oldValue = accessor.valueRef.value || [] + const oldValue = accessor.value || [] const newValue = [...oldValue] const checkValueIndex = newValue.indexOf(value) if (checkValueIndex === -1) { @@ -151,15 +152,16 @@ const useCheckbox = ( callEmit(groupProps.onChange, newValue, oldValue) } } else { - const accessor = useFormAccessor('checked') + const { accessor, control } = useAccessorAndControl({ valueKey: 'checked' }) + useFormItemRegister(control) - isChecked = computed(() => accessor.valueRef.value === props.trueValue) - isDisabled = computed(() => accessor.disabled.value) + isChecked = computed(() => accessor.value === props.trueValue) + isDisabled = computed(() => accessor.disabled) handleBlur = (evt: FocusEvent) => { isFocused.value = false - callEmit(props.onBlur, evt) accessor.markAsBlurred() + callEmit(props.onBlur, evt) } handleChange = (evt: Event) => { diff --git a/packages/components/checkbox/src/CheckboxGroup.tsx b/packages/components/checkbox/src/CheckboxGroup.tsx index 2a5642b53..d57eb8cbc 100644 --- a/packages/components/checkbox/src/CheckboxGroup.tsx +++ b/packages/components/checkbox/src/CheckboxGroup.tsx @@ -9,9 +9,10 @@ import { type VNodeChild, computed, defineComponent, normalizeClass, provide } f import { isNil } from 'lodash-es' +import { useAccessorAndControl } from '@idux/cdk/forms' import { Logger, convertCssPixel } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' -import { useFormAccessor } from '@idux/components/form' +import { useFormItemRegister } from '@idux/components/form' import { IxSpace } from '@idux/components/space' import Checkbox from './Checkbox' @@ -24,7 +25,8 @@ export default defineComponent({ setup(props, { slots }) { const common = useGlobalConfig('common') const mergedPrefixCls = computed(() => `${common.prefixCls}-checkbox-group`) - const accessor = useFormAccessor() + const { accessor, control } = useAccessorAndControl() + useFormItemRegister(control) provide(checkboxGroupToken, { props, accessor }) const classes = computed(() => { diff --git a/packages/components/checkbox/src/token.ts b/packages/components/checkbox/src/token.ts index e2cf1eed7..0845bee0f 100644 --- a/packages/components/checkbox/src/token.ts +++ b/packages/components/checkbox/src/token.ts @@ -6,12 +6,12 @@ */ import type { CheckboxGroupProps } from './types' -import type { ValueAccessor } from '@idux/cdk/forms' +import type { FormAccessor } from '@idux/cdk/forms' import type { InjectionKey } from 'vue' export interface CheckboxGroupContext { props: CheckboxGroupProps - accessor: ValueAccessor + accessor: FormAccessor } export const checkboxGroupToken: InjectionKey = Symbol('checkboxGroupToken') diff --git a/packages/components/date-picker/src/DatePicker.tsx b/packages/components/date-picker/src/DatePicker.tsx index e3a204763..7bf343a86 100644 --- a/packages/components/date-picker/src/DatePicker.tsx +++ b/packages/components/date-picker/src/DatePicker.tsx @@ -5,7 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, nextTick, normalizeClass, provide, watch } from 'vue' +import { computed, defineComponent, nextTick, normalizeClass, provide, toRef, watch } from 'vue' import { ɵOverlay } from '@idux/components/_private/overlay' import { useDateConfig, useGlobalConfig } from '@idux/components/config' @@ -44,7 +44,13 @@ export default defineComponent({ const { accessor, handleChange } = pickerStateContext - const controlContext = useControl(dateConfig, formatContext, inputEnableStatus, accessor.valueRef, handleChange) + const controlContext = useControl( + dateConfig, + formatContext, + inputEnableStatus, + toRef(accessor, 'value'), + handleChange, + ) const { overlayOpened, overlayVisible, onAfterLeave, setOverlayOpened } = useOverlayState(props, controlContext) const handleKeyDown = useKeyboardEvents(setOverlayOpened) diff --git a/packages/components/date-picker/src/DateRangePicker.tsx b/packages/components/date-picker/src/DateRangePicker.tsx index f9da19e12..3c3b47a95 100644 --- a/packages/components/date-picker/src/DateRangePicker.tsx +++ b/packages/components/date-picker/src/DateRangePicker.tsx @@ -5,7 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, nextTick, normalizeClass, provide, watch } from 'vue' +import { computed, defineComponent, nextTick, normalizeClass, provide, toRef, watch } from 'vue' import { ɵOverlay } from '@idux/components/_private/overlay' import { useDateConfig, useGlobalConfig } from '@idux/components/config' @@ -44,7 +44,7 @@ export default defineComponent({ const { accessor, handleChange } = pickerStateContext - const rangeControlContext = useRangeControl(dateConfig, formatContext, inputEnableStatus, accessor.valueRef) + const rangeControlContext = useRangeControl(dateConfig, formatContext, inputEnableStatus, toRef(accessor, 'value')) const { overlayOpened, overlayVisible, onAfterLeave, setOverlayOpened } = useOverlayState( props, rangeControlContext, diff --git a/packages/components/date-picker/src/composables/useControl.ts b/packages/components/date-picker/src/composables/useControl.ts index b78708a70..7b07e98b5 100644 --- a/packages/components/date-picker/src/composables/useControl.ts +++ b/packages/components/date-picker/src/composables/useControl.ts @@ -5,15 +5,14 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { FormatContext } from './useFormat' -import type { InputEnableStatus } from './useInputEnableStatus' -import type { DateConfig } from '@idux/components/config' - -import { type ComputedRef, watch } from 'vue' +import { type ComputedRef, type Ref, watch } from 'vue' import { useState } from '@idux/cdk/utils' +import { type DateConfig } from '@idux/components/config' import { applyDateTime, convertToDate, isSameDateTime } from '../utils' +import { type FormatContext } from './useFormat' +import { type InputEnableStatus } from './useInputEnableStatus' export interface PickerControlContext { inputValue: ComputedRef @@ -43,7 +42,7 @@ export function useControl( dateConfig: DateConfig, formatContext: FormatContext, inputEnableStatus: ComputedRef, - valueRef: ComputedRef, + valueRef: Ref, handleChange: (value: Date | undefined) => void, ): PickerControlContext { const { formatRef, dateFormatRef, timeFormatRef } = formatContext diff --git a/packages/components/date-picker/src/composables/useInputProps.ts b/packages/components/date-picker/src/composables/useInputProps.ts index 9b5ce4f7b..b90663823 100644 --- a/packages/components/date-picker/src/composables/useInputProps.ts +++ b/packages/components/date-picker/src/composables/useInputProps.ts @@ -19,7 +19,7 @@ export function useInputProps(context: DatePickerContext | DateRangePickerContex borderless: false, clearable: props.clearable ?? config.clearable, clearIcon: props.clearIcon ?? config.clearIcon, - disabled: accessor.disabled.value, + disabled: accessor.disabled, size: 'sm', } }) diff --git a/packages/components/date-picker/src/composables/useInputState.ts b/packages/components/date-picker/src/composables/useInputState.ts index adc19cdb8..f3a0cb3cb 100644 --- a/packages/components/date-picker/src/composables/useInputState.ts +++ b/packages/components/date-picker/src/composables/useInputState.ts @@ -7,14 +7,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { DatePickerProps } from '../types' -import type { ValueAccessor } from '@idux/cdk/forms' -import type { DateConfig } from '@idux/components/config' -import type { ComputedRef, Ref } from 'vue' - -import { toRaw, watch } from 'vue' +import { type ComputedRef, type Ref, toRaw, watch } from 'vue' +import { type FormAccessor } from '@idux/cdk/forms' import { callEmit, useState } from '@idux/cdk/utils' +import { type DateConfig } from '@idux/components/config' + +import { type DatePickerProps } from '../types' export interface InputStateContext { inputValue: Ref @@ -28,28 +27,29 @@ export interface InputStateContext { export function useInputState( props: DatePickerProps, dateConfig: DateConfig, - accessor: ValueAccessor, + accessor: FormAccessor, formatRef: ComputedRef, ): InputStateContext { - const initValue = accessor.valueRef.value + const initValue = accessor.value const formatText = formatRef.value - const initText = initValue - ? dateConfig.format(dateConfig.convert(accessor.valueRef.value, formatText), formatText) - : '' + const initText = initValue ? dateConfig.format(dateConfig.convert(initValue, formatText), formatText) : '' const [inputValue, setInputValue] = useState(initText) const [isFocused, setFocused] = useState(false) - watch(accessor.valueRef, value => { - if (!value) { - setInputValue('') - return - } - const formatText = formatRef.value - const newValue = dateConfig.format(dateConfig.convert(value, formatText), formatText) - if (newValue !== inputValue.value) { - setInputValue(newValue) - } - }) + watch( + () => accessor.value, + value => { + if (!value) { + setInputValue('') + return + } + const formatText = formatRef.value + const newValue = dateConfig.format(dateConfig.convert(value, formatText), formatText) + if (newValue !== inputValue.value) { + setInputValue(newValue) + } + }, + ) const handleInput = (evt: Event) => { callEmit(props.onInput, evt) @@ -64,26 +64,26 @@ export function useInputState( if (!isValid(currDate) || format(currDate, formatText) != value) { return } - const oldDate = toRaw(accessor.valueRef.value) + const oldDate = toRaw(accessor.value) accessor.setValue(currDate) callEmit(props.onChange, currDate, oldDate) } const handleFocus = (evt: FocusEvent) => { - callEmit(props.onFocus, evt) setFocused(true) + callEmit(props.onFocus, evt) } const handleBlur = (evt: FocusEvent) => { - callEmit(props.onBlur, evt) - accessor.markAsBlurred() setFocused(false) + accessor.markAsBlurred() + callEmit(props.onBlur, evt) } const handleClear = (evt: MouseEvent) => { - callEmit(props.onClear, evt) evt.stopPropagation() accessor.setValue(undefined) + callEmit(props.onClear, evt) } return { diff --git a/packages/components/date-picker/src/composables/useOverlayProps.ts b/packages/components/date-picker/src/composables/useOverlayProps.ts index cdc541381..8b7a7d849 100644 --- a/packages/components/date-picker/src/composables/useOverlayProps.ts +++ b/packages/components/date-picker/src/composables/useOverlayProps.ts @@ -16,7 +16,7 @@ export function useOverlayProps(context: DatePickerContext | DateRangePickerCont const { props, config, accessor, mergedPrefixCls, overlayOpened, setOverlayOpened, onAfterLeave } = context return { clickOutside: true, - disabled: accessor.disabled.value || props.readonly, + disabled: accessor.disabled || props.readonly, offset: defaultOffset, placement: 'bottomStart', transitionName: 'ix-fade', diff --git a/packages/components/date-picker/src/composables/usePickerState.ts b/packages/components/date-picker/src/composables/usePickerState.ts index 27eb43a2d..ef6b0bab5 100644 --- a/packages/components/date-picker/src/composables/usePickerState.ts +++ b/packages/components/date-picker/src/composables/usePickerState.ts @@ -5,17 +5,16 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { DatePickerProps, DateRangePickerProps } from '../types' -import type { ValueAccessor } from '@idux/cdk/forms' -import type { DateConfig } from '@idux/components/config' - import { type ComputedRef, toRaw } from 'vue' import { isArray } from 'lodash-es' +import { type FormAccessor, useAccessorAndControl } from '@idux/cdk/forms' import { callEmit, useState } from '@idux/cdk/utils' -import { useFormAccessor } from '@idux/components/form' +import { type DateConfig } from '@idux/components/config' +import { useFormItemRegister } from '@idux/components/form' +import { type DatePickerProps, type DateRangePickerProps } from '../types' import { convertToDate, sortRangeValue } from '../utils' type StateValueType = T extends DatePickerProps @@ -23,10 +22,10 @@ type StateValueType = T extend : (Date | undefined)[] | undefined export interface PickerStateContext { - accessor: ValueAccessor + accessor: FormAccessor isFocused: ComputedRef handleChange: (value: StateValueType) => void - handleClear: (evt: Event) => void + handleClear: (evt: MouseEvent) => void handleFocus: (evt: FocusEvent) => void handleBlur: (evt: FocusEvent) => void } @@ -36,44 +35,37 @@ export function usePickerState dateConfig: DateConfig, formatRef: ComputedRef, ): PickerStateContext { - const accessor = useFormAccessor() + const { accessor, control } = useAccessorAndControl() + useFormItemRegister(control) const [isFocused, setFocused] = useState(false) function handleChange(value: StateValueType) { const newValue = (isArray(value) ? sortRangeValue(dateConfig, value) : value) as StateValueType - let oldValue = toRaw(accessor.valueRef.value) as StateValueType + let oldValue = toRaw(accessor.value) as StateValueType oldValue = ( isArray(oldValue) ? oldValue.map(v => convertToDate(dateConfig, v, formatRef.value)) : convertToDate(dateConfig, oldValue, formatRef.value) ) as StateValueType - accessor.setValue(value) + accessor.setValue(value as T['value']) callEmit(props.onChange as (value: StateValueType, oldValue: StateValueType) => void, newValue, oldValue) } - function handleClear(evt: Event) { - let oldValue = toRaw(accessor.valueRef.value) as StateValueType - oldValue = ( - isArray(oldValue) - ? oldValue.map(v => convertToDate(dateConfig, v, formatRef.value)) - : convertToDate(dateConfig, oldValue, formatRef.value) - ) as StateValueType - - accessor.setValue(undefined) - callEmit(props.onClear, evt as MouseEvent) - callEmit(props.onChange as (value: StateValueType, oldValue: StateValueType) => void, undefined, oldValue) + function handleClear(evt: MouseEvent) { + callEmit(props.onClear, evt) + handleChange(undefined) } function handleFocus(evt: FocusEvent) { - callEmit(props.onFocus, evt) setFocused(true) + callEmit(props.onFocus, evt) } function handleBlur(evt: FocusEvent) { + setFocused(false) accessor.markAsBlurred() callEmit(props.onBlur, evt) - setFocused(false) } return { diff --git a/packages/components/date-picker/src/composables/useRangeControl.ts b/packages/components/date-picker/src/composables/useRangeControl.ts index 9a9c85622..ce4041d2a 100644 --- a/packages/components/date-picker/src/composables/useRangeControl.ts +++ b/packages/components/date-picker/src/composables/useRangeControl.ts @@ -5,16 +5,15 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { FormatContext } from './useFormat' -import type { InputEnableStatus } from './useInputEnableStatus' -import type { DateConfig } from '@idux/components/config' - -import { type ComputedRef, computed, watch } from 'vue' +import { type ComputedRef, type Ref, computed, watch } from 'vue' import { convertArray, useState } from '@idux/cdk/utils' +import { type DateConfig } from '@idux/components/config' import { compareDateTime, convertToDate, sortRangeValue } from '../utils' import { type PickerControlContext, useControl } from './useControl' +import { type FormatContext } from './useFormat' +import { type InputEnableStatus } from './useInputEnableStatus' export interface PickerRangeControlContext { buffer: ComputedRef<(Date | undefined)[] | undefined> @@ -34,7 +33,7 @@ export function useRangeControl( dateConfig: DateConfig, formatContext: FormatContext, inputEnableStatus: ComputedRef, - valueRef: ComputedRef<(string | number | Date | undefined)[] | undefined>, + valueRef: Ref<(string | number | Date)[] | undefined>, ): PickerRangeControlContext { const { formatRef } = formatContext const [buffer, setBuffer] = useState<(Date | undefined)[] | undefined>( diff --git a/packages/components/date-picker/src/composables/useTriggerProps.ts b/packages/components/date-picker/src/composables/useTriggerProps.ts index c02278e5e..c6b4e3afb 100644 --- a/packages/components/date-picker/src/composables/useTriggerProps.ts +++ b/packages/components/date-picker/src/composables/useTriggerProps.ts @@ -31,7 +31,7 @@ export function useTriggerProps( const handleClick = () => { const currOpened = overlayOpened.value - if (currOpened || accessor.disabled.value) { + if (currOpened || accessor.disabled) { return } @@ -41,13 +41,9 @@ export function useTriggerProps( return computed(() => { return { borderless: props.borderless, - clearable: - !props.readonly && - !accessor.disabled.value && - (props.clearable ?? config.clearable) && - !!accessor.valueRef.value, + clearable: !props.readonly && !accessor.disabled && (props.clearable ?? config.clearable) && !!accessor.value, clearIcon: props.clearIcon ?? config.clearIcon, - disabled: accessor.disabled.value, + disabled: accessor.disabled, focused: isFocused.value, readonly: props.readonly || inputEnableStatus.value.enableInput === false, size: props.size ?? formContext?.size.value ?? config.size, diff --git a/packages/components/date-picker/src/types.ts b/packages/components/date-picker/src/types.ts index 491faf17b..cf7fac9d6 100644 --- a/packages/components/date-picker/src/types.ts +++ b/packages/components/date-picker/src/types.ts @@ -99,7 +99,7 @@ export interface DatePickerCommonBindings { export const datePickerProps = { ...datePickerCommonProps, - value: [String, Date, Number], + value: [String, Date, Number] as PropType, footer: { type: [Boolean, Array, Object] as PropType, default: false }, placeholder: String, timePanelOptions: Object as PropType, @@ -119,7 +119,7 @@ export type DatePickerInstance = InstanceType, + value: Array as PropType<(number | string | Date)[]>, footer: { type: [Boolean, Array, Object] as PropType, default: true }, placeholder: Array as PropType, separator: [String, Object] as PropType, diff --git a/packages/components/form/demo/Basic.vue b/packages/components/form/demo/Basic.vue index 6a673fa8a..15de0464c 100644 --- a/packages/components/form/demo/Basic.vue +++ b/packages/components/form/demo/Basic.vue @@ -1,69 +1,34 @@ - - - - +const value = ref('tom') + diff --git a/packages/components/form/docs/Index.zh.md b/packages/components/form/docs/Index.zh.md index ffd3be854..8b59dc93c 100644 --- a/packages/components/form/docs/Index.zh.md +++ b/packages/components/form/docs/Index.zh.md @@ -54,7 +54,7 @@ order: 0 ### IxFormWrapper -用于嵌套表单时, 简于子组件的 `control` 路径 +用于嵌套表单时, 简化子组件的 `control` 路径 #### FormWrapperProps @@ -62,6 +62,14 @@ order: 0 | --- | --- | --- | --- | --- | --- | | `control` | 表单控件的控制器 | `string \| number \| AbstractControl` | - | - | - | +### useFormItemRegister + +表单组件注册 control, 配合 `useAccessorAndControl` 使用 + +```ts +function useFormItemRegister(control: ShallowRef): void +``` + ## FAQ ### 自定义表单组件 @@ -70,48 +78,36 @@ order: 0 ```html - ``` diff --git a/packages/components/form/src/Form.tsx b/packages/components/form/src/Form.tsx index 462ac4002..bd44f7f6a 100644 --- a/packages/components/form/src/Form.tsx +++ b/packages/components/form/src/Form.tsx @@ -7,7 +7,7 @@ import { computed, defineComponent, normalizeClass, provide } from 'vue' -import { FORMS_CONTROL_TOKEN, useValueControl } from '@idux/cdk/forms' +import { FORMS_CONTROL_TOKEN, useControl } from '@idux/cdk/forms' import { useGlobalConfig } from '@idux/components/config' import { FORM_TOKEN, formToken } from './token' @@ -17,7 +17,7 @@ export default defineComponent({ name: 'IxForm', props: formProps, setup(props, { slots }) { - const control = useValueControl() + const control = useControl() provide(FORMS_CONTROL_TOKEN, control) const common = useGlobalConfig('common') diff --git a/packages/components/form/src/FormWrapper.tsx b/packages/components/form/src/FormWrapper.tsx index 84d2ae7db..7851ea0b9 100644 --- a/packages/components/form/src/FormWrapper.tsx +++ b/packages/components/form/src/FormWrapper.tsx @@ -7,7 +7,7 @@ import { defineComponent, provide } from 'vue' -import { FORMS_CONTROL_TOKEN, useValueControl } from '@idux/cdk/forms' +import { FORMS_CONTROL_TOKEN, useControl } from '@idux/cdk/forms' import { formWrapperProps } from './types' @@ -15,7 +15,7 @@ export default defineComponent({ name: 'IxFormWrapper', props: formWrapperProps, setup(_, { slots }) { - const control = useValueControl() + const control = useControl() provide(FORMS_CONTROL_TOKEN, control) return () => slots.default?.() diff --git a/packages/components/form/src/composables/public.ts b/packages/components/form/src/composables/public.ts index ab9c25d33..91c78703b 100644 --- a/packages/components/form/src/composables/public.ts +++ b/packages/components/form/src/composables/public.ts @@ -5,7 +5,17 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { ComputedRef, type Ref, type WatchStopHandle, computed, inject, onBeforeUnmount, ref, watch } from 'vue' +import { + type ComputedRef, + type Ref, + type ShallowRef, + type WatchStopHandle, + computed, + inject, + onBeforeUnmount, + ref, + watch, +} from 'vue' import { useSharedFocusMonitor } from '@idux/cdk/a11y' import { type AbstractControl, type ValueAccessor, useValueAccessor, useValueControl } from '@idux/cdk/forms' @@ -14,7 +24,7 @@ import { useKey } from '@idux/components/utils' import { FORM_ITEM_TOKEN, FORM_TOKEN } from '../token' import { type FormSize } from '../types' -export function useFormItemRegister(control: Ref): void { +export function useFormItemRegister(control: ShallowRef): void { const context = inject(FORM_ITEM_TOKEN, null) if (context) { const key = useKey() @@ -25,11 +35,7 @@ export function useFormItemRegister(control: Ref): } /** - * 使用 valueAccessor 接管 value 以及 disabled 状态的控制 - * 同时在 IxFormItem 中注册 control, 可以与 IxFormItem 同步状态 - * - * @param valueKey - props 中表是 value 的字段名 - * @returns + * @deprecated 使用 useAccessorAndControl + useFormItemRegister 替代 */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useFormAccessor(valueKey?: string): ValueAccessor { diff --git a/packages/components/form/src/composables/useFormItem.ts b/packages/components/form/src/composables/useFormItem.ts index f596ddab7..f382a0cc5 100644 --- a/packages/components/form/src/composables/useFormItem.ts +++ b/packages/components/form/src/composables/useFormItem.ts @@ -18,7 +18,7 @@ import { import { isFunction, isObject, isString } from 'lodash-es' -import { type AbstractControl, type ValidateStatus, useValueControl } from '@idux/cdk/forms' +import { type AbstractControl, type ValidateStatus, useControl as useSelfControl } from '@idux/cdk/forms' import { Logger, type VKey } from '@idux/cdk/utils' import { useGlobalConfig } from '@idux/components/config' import { type Locale } from '@idux/components/locales' @@ -66,7 +66,7 @@ function useControl() { provide(FORM_ITEM_TOKEN, { registerControl, unregisterControl }) - const selfControl = useValueControl() + const selfControl = useSelfControl() const control = shallowRef() diff --git a/packages/components/input-number/src/useInputNumber.ts b/packages/components/input-number/src/useInputNumber.ts index bb4df5d02..4995f963c 100644 --- a/packages/components/input-number/src/useInputNumber.ts +++ b/packages/components/input-number/src/useInputNumber.ts @@ -11,8 +11,9 @@ import type { ComputedRef, Ref } from 'vue' import { computed, nextTick, ref, toRaw, watch } from 'vue' +import { useAccessorAndControl } from '@idux/cdk/forms' import { Logger, callEmit } from '@idux/cdk/utils' -import { useFormAccessor } from '@idux/components/form' +import { useFormItemRegister } from '@idux/components/form' export interface InputNumberBindings { displayValue: Ref @@ -32,13 +33,14 @@ export interface InputNumberBindings { } export function useInputNumber(props: InputNumberProps, config: InputNumberConfig): InputNumberBindings { - const accessor = useFormAccessor() + const { accessor, control } = useAccessorAndControl() + useFormItemRegister(control) const displayValue = ref('') const isIllegal = ref(true) - const nowValue = computed(() => accessor.valueRef.value ?? undefined) + const nowValue = computed(() => accessor.value ?? undefined) const isKeyboard = computed(() => props.keyboard ?? config.keyboard) - const isDisabled = computed(() => accessor?.disabled.value) + const isDisabled = computed(() => accessor.disabled) const isDisabledDec = computed(() => props.readonly || (!!nowValue.value && nowValue.value <= props.min)) const isDisabledInc = computed(() => props.readonly || (!!nowValue.value && nowValue.value >= props.max)) @@ -53,14 +55,14 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi } return props.precision } - return Math.max(getPrecision(accessor.valueRef.value), stepPrecision) + return Math.max(getPrecision(accessor.value), stepPrecision) }) const disabledDec = computed(() => getIncValueFormAccessor(-props.step) < props.min) const disabledInc = computed(() => getIncValueFormAccessor(props.step) > props.max) function getIncValueFormAccessor(step: number) { - const { value } = accessor.valueRef + const { value } = accessor let newVal = step if (typeof value === 'number' && !Number.isNaN(value)) { // Use the toFixed func to ensure numerical accuracy. @@ -71,7 +73,7 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi } function updateDisplayValueFromAccessor() { - const { value } = accessor.valueRef + const { value } = accessor if (value === null || value === undefined) { displayValue.value = '' } else if (Number.isNaN(value)) { @@ -103,13 +105,13 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi } function updateModelValue(newVal: number | null) { - const oldVal = toRaw(accessor.valueRef.value) + const oldVal = toRaw(accessor.value) if (newVal !== oldVal) { accessor.setValue(newVal) callEmit(props.onChange, newVal, oldVal) nextTick(() => { - if (newVal !== accessor.valueRef.value) { + if (newVal !== accessor.value) { updateDisplayValueFromAccessor() } }) @@ -176,8 +178,8 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi function handleBlur(evt: FocusEvent) { isFocused.value = false updateModelValueFromDisplayValue() - callEmit(props.onBlur, evt) accessor.markAsBlurred() + callEmit(props.onBlur, evt) } watch( @@ -193,7 +195,11 @@ export function useInputNumber(props: InputNumberProps, config: InputNumberConfi { immediate: true }, ) - watch(accessor.valueRef, () => updateDisplayValueFromAccessor(), { immediate: true }) + watch( + () => accessor.value, + () => updateDisplayValueFromAccessor(), + { immediate: true }, + ) return { displayValue, diff --git a/packages/components/input/src/Input.tsx b/packages/components/input/src/Input.tsx index 4a28c3ec9..41bccf90d 100644 --- a/packages/components/input/src/Input.tsx +++ b/packages/components/input/src/Input.tsx @@ -62,7 +62,7 @@ export default defineComponent({ clearable={clearable.value} clearIcon={clearIcon.value} clearVisible={clearVisible.value} - disabled={accessor.disabled.value} + disabled={accessor.disabled} focused={isFocused.value} prefix={prefix} size={mergedSize.value} diff --git a/packages/components/input/src/useInput.ts b/packages/components/input/src/useInput.ts index 49a722b00..aebdae56f 100644 --- a/packages/components/input/src/useInput.ts +++ b/packages/components/input/src/useInput.ts @@ -6,18 +6,18 @@ */ import type { CommonProps } from './types' -import type { ValueAccessor } from '@idux/cdk/forms' import type { InputConfig, TextareaConfig } from '@idux/components/config' import type { ComputedRef, Ref } from 'vue' import { computed, nextTick, ref, toRaw, watch } from 'vue' +import { FormAccessor, useAccessorAndControl } from '@idux/cdk/forms' import { callEmit } from '@idux/cdk/utils' -import { useFormAccessor, useFormFocusMonitor } from '@idux/components/form' +import { useFormFocusMonitor, useFormItemRegister } from '@idux/components/form' export interface InputContext { elementRef: Ref - accessor: ValueAccessor + accessor: FormAccessor clearIcon: ComputedRef clearVisible: ComputedRef clearable: ComputedRef @@ -39,11 +39,15 @@ export function useInput( props: CommonProps, config: InputConfig | TextareaConfig, ): InputContext { - const accessor = useFormAccessor() + // const control = useValueControl() + // const accessor = useValueAccessor({ control }) + // useFormItemRegister(control) + const { accessor, control } = useAccessorAndControl() + useFormItemRegister(control) const clearable = computed(() => props.clearable ?? config.clearable) const clearIcon = computed(() => props.clearIcon ?? config.clearIcon) - const clearVisible = computed(() => !accessor.disabled.value && !props.readonly && !!accessor.valueRef.value) + const clearVisible = computed(() => !accessor.disabled && !props.readonly && !!accessor.value) const isFocused = ref(false) const handleFocus = (evt: FocusEvent) => { @@ -52,8 +56,8 @@ export function useInput( } const handleBlur = (evt: FocusEvent) => { isFocused.value = false - callEmit(props.onBlur, evt) accessor.markAsBlurred() + callEmit(props.onBlur, evt) if (props.trim) { setValue((evt.target as HTMLInputElement).value.trim()) @@ -66,7 +70,7 @@ export function useInput( }) const setValue = (value: string) => { - const oldValue = toRaw(accessor.valueRef.value) + const oldValue = toRaw(accessor.value) if (value !== oldValue) { accessor.setValue(value) callEmit(props.onChange, value, oldValue) @@ -78,13 +82,16 @@ export function useInput( const syncValue = () => { const element = elementRef.value - const value = accessor.valueRef.value ?? '' + const value = accessor.value ?? '' if (element && element.value !== value) { element.value = value } } - watch(accessor.valueRef, () => syncValue()) + watch( + () => accessor.value, + () => syncValue(), + ) const isComposing = ref(false) const handleInput = (evt: Event, emitInput = true) => { @@ -109,8 +116,8 @@ export function useInput( } const handleClear = (evt: MouseEvent) => { - callEmit(props.onClear, evt) accessor.setValue('') + callEmit(props.onClear, evt) } return { diff --git a/packages/components/radio/demo/Group.vue b/packages/components/radio/demo/Group.vue index 9a02f68d6..86542d60d 100644 --- a/packages/components/radio/demo/Group.vue +++ b/packages/components/radio/demo/Group.vue @@ -1,5 +1,5 @@