`,
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..4f31521fb 100644
--- a/packages/cdk/forms/src/utils.ts
+++ b/packages/cdk/forms/src/utils.ts
@@ -7,28 +7,182 @@
/* 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'
+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 }
+}
+
/**
* @deprecated
*/
-export const controlPropDef = [String, Number, Object] as PropType
-
-export const FORMS_CONTROL_TOKEN: InjectionKey> = Symbol('cdk-forms-control')
-
export interface ValueControlOptions {
controlKey?: string
}
+/**
+ * @deprecated please use `useControl` or `useAccessorAndControl` instead
+ */
export function useValueControl(
options: ValueControlOptions = {},
): ShallowRef | undefined> {
@@ -68,12 +222,18 @@ export function useValueControl(
return control
}
+/**
+ * @deprecated
+ */
export interface ValueAccessorOptions {
control: ShallowRef | undefined>
valueKey?: string
disabledKey?: string
}
+/**
+ * @deprecated
+ */
export interface ValueAccessor {
valueRef: ComputedRef
disabled: ComputedRef
@@ -81,6 +241,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 +265,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