From 1163d27f71abbe1b6827cc5bb96914c4e956720b Mon Sep 17 00:00:00 2001 From: kooriookami <38392315+kooriookami@users.noreply.github.com> Date: Fri, 12 Apr 2024 13:33:21 +0800 Subject: [PATCH] feat(components): add empty-values and value-on-clear props (#16361) * feat(components): add empty values * feat(hooks): update * feat(components): update * feat(components): update * feat: update * feat(components): update * feat(components): update * feat(components): update * feat: update doc * feat: add doc --- docs/en-US/component/cascader.md | 20 ++--- docs/en-US/component/config-provider.md | 51 +++++++++--- docs/en-US/component/date-picker.md | 66 +++++++-------- docs/en-US/component/datetime-picker.md | 68 ++++++++-------- docs/en-US/component/select-v2.md | 24 ++++-- docs/en-US/component/select.md | 30 +++++-- docs/en-US/component/time-picker.md | 62 +++++++------- docs/en-US/component/time-select.md | 42 +++++----- .../examples/config-provider/empty-values.vue | 69 ++++++++++++++++ docs/examples/select-v2/empty-values.vue | 35 ++++++++ docs/examples/select/empty-values.vue | 56 +++++++++++++ packages/components/cascader/src/cascader.ts | 9 ++- packages/components/cascader/src/cascader.vue | 8 +- .../src/config-provider-props.ts | 3 +- .../src/hooks/use-global-config.ts | 2 +- .../date-picker/__tests__/date-picker.test.ts | 4 +- packages/components/select-v2/src/defaults.ts | 3 +- .../components/select-v2/src/useSelect.ts | 16 ++-- packages/components/select/src/select.ts | 3 +- packages/components/select/src/useSelect.ts | 16 ++-- .../time-picker/src/common/picker.vue | 11 +-- .../time-picker/src/common/props.ts | 3 +- .../components/time-select/src/time-select.ts | 3 +- .../time-select/src/time-select.vue | 2 + .../hooks/__tests__/use-empty-values.test.tsx | 81 +++++++++++++++++++ packages/hooks/index.ts | 1 + packages/hooks/use-empty-values/index.ts | 64 +++++++++++++++ 27 files changed, 564 insertions(+), 188 deletions(-) create mode 100644 docs/examples/config-provider/empty-values.vue create mode 100644 docs/examples/select-v2/empty-values.vue create mode 100644 docs/examples/select/empty-values.vue create mode 100644 packages/hooks/__tests__/use-empty-values.test.tsx create mode 100644 packages/hooks/use-empty-values/index.ts diff --git a/docs/en-US/component/cascader.md b/docs/en-US/component/cascader.md index aacec4e1f2..1f7dffad33 100644 --- a/docs/en-US/component/cascader.md +++ b/docs/en-US/component/cascader.md @@ -138,7 +138,7 @@ cascader/panel ### Cascader Attributes | Name | Description | Type | Default | -| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ------- | +|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|---------| | model-value / v-model | binding value | ^[string]/^[number]/^[object]`string[] \| number[] \| any` | — | | options | data of the options, the key of `value` and `label` can be customize by `CascaderProps`. | ^[object]`Record[]` | — | | props | configuration options, see the following `CascaderProps` table. | ^[object]`CascaderProps` | — | @@ -160,11 +160,13 @@ cascader/panel | tag-type | tag type | ^[enum]`'success' \| 'info' \| 'warning' \| 'danger'` | info | | validate-event | whether to trigger form validation | ^[boolean] | true | | max-collapse-tags ^(2.3.10) | The max tags number to be shown. To use this, `collpase-tags` must be true | ^[number] | 1 | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ### Cascader Events | Name | Description | Type | -| -------------- | --------------------------------------------------- | ----------------------------------------------------------- | +|----------------|-----------------------------------------------------|-------------------------------------------------------------| | change | triggers when the binding value changes | ^[Function]`(value: CascaderValue) => void` | | expand-change | triggers when expand option changes | ^[Function]`(value: CascaderValue) => void` | | blur | triggers when Cascader blurs | ^[Function]`(event: FocusEvent) => void` | @@ -175,14 +177,14 @@ cascader/panel ### Cascader Slots | Name | Description | Scope | -| ------- | ---------------------------------------------------------------------------------------------- | ----------------------------------- | +|---------|------------------------------------------------------------------------------------------------|-------------------------------------| | default | the custom content of cascader node, which are current Node object and node data respectively. | ^[object]`{ node: any, data: any }` | | empty | content when there is no matched options. | — | ### Cascader Exposes | Name | Description | Type | -| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +|-------------------------------|-------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| | getCheckedNodes | get an array of currently selected node,(leafOnly) whether only return the leaf checked nodes, default is `false` | ^[Function]`(leafOnly: boolean) => CascaderNode[] \| undefined` | | cascaderPanelRef | cascader panel ref | ^[object]`ComputedRef` | | togglePopperVisible ^(2.2.31) | toggle the visible type of popper | ^[Function]`(visible?: boolean) => void` | @@ -193,7 +195,7 @@ cascader/panel ### CascaderPanel Attributes | Name | Description | Type | Default | -| --------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ------- | +|-----------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------|---------| | model-value / v-model | binding value | ^[string]/^[number]/^[object]`string[] \| number[] \| any` | — | | options | data of the options, the key of `value` and `label` can be customize by `CascaderProps`. | ^[object]`Record[]` | — | | props | configuration options, see the following `CascaderProps` table. | ^[object]`CascaderProps` | — | @@ -201,7 +203,7 @@ cascader/panel ### CascaderPanel Events | Name | Description | Type | -| ------------- | ----------------------------------------------------------------------- | --------------------------------------------------- | +|---------------|-------------------------------------------------------------------------|-----------------------------------------------------| | change | triggers when the binding value changes | ^[Function]`(value: CascaderValue) => void` | | expand-change | triggers when expand option changes | ^[Function]`(value: CascaderNodePathValue) => void` | | close | close panel event, provided to Cascader to put away the panel judgment. | ^[Function]`() => void` | @@ -209,20 +211,20 @@ cascader/panel ### CascaderPanel Slots | Name | Description | Scope | -| ------- | ---------------------------------------------------------------------------------------------- | ----------------------------------- | +|---------|------------------------------------------------------------------------------------------------|-------------------------------------| | default | the custom content of cascader node, which are current Node object and node data respectively. | ^[object]`{ node: any, data: any }` | ### CascaderPanel Exposes | Name | Description | Type | -| ----------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +|-------------------|-------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| | getCheckedNodes | get an array of currently selected node,(leafOnly) whether only return the leaf checked nodes, default is `false` | ^[Function]`(leafOnly: boolean) => CascaderNode[] \| undefined` | | clearCheckedNodes | clear checked nodes | ^[Function]`() => void` | ## CascaderProps | Attribute | Description | Type | Default | -| -------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -------- | +|----------------|------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|----------| | expandTrigger | trigger mode of expanding options | ^[enum]`'click' \| 'hover'` | click | | multiple | whether multiple selection is enabled | ^[boolean] | false | | checkStrictly | whether checked state of a node not affects its parent and child nodes | ^[boolean] | false | diff --git a/docs/en-US/component/config-provider.md b/docs/en-US/component/config-provider.md index edc9c1fd2d..475d4b0bb4 100644 --- a/docs/en-US/component/config-provider.md +++ b/docs/en-US/component/config-provider.md @@ -33,6 +33,31 @@ config-provider/message ::: +## Empty Values Configurations ^(2.7.0) + +
+ Supported components list + +- Cascader +- DatePicker +- Select +- SelectV2 +- TimePicker +- TimeSelect +- TreeSelect + +
+ +Set `empty-values` to support empty values of components. The fallback value is `['', null, undefined]`. If you think the empty string is meaningful, write `[undefined, null]`. + +Set `value-on-clear` to set the return value when cleared. The fallback value is `undefined`. In the date component is `null`. If you want to set `undefined`, use `() => undefined`. + +:::demo + +config-provider/empty-values + +::: + ## Experimental features In this section, you can learn how to use Config Provider to provide experimental features. For now, we haven't added any experimental features, but in the feature roadmap, we will add some experimental features. You can use this config to manage the features you want or not. @@ -43,30 +68,32 @@ In this section, you can learn how to use Config Provider to provide experimenta ### Config Provider Attributes -| Name | Description | Type | Default | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| locale | Locale Object | ^[object]`{name: string, el: TranslatePair}`[](https://github.com/element-plus/element-plus/blob/a98ff9b40c0c3d2b9959f99919bd8363e3e3c25a/packages/locale/index.ts#L5) [languages](https://github.com/element-plus/element-plus/tree/dev/packages/locale/lang) | [en](https://github.com/element-plus/element-plus/blob/dev/packages/locale/lang/en.ts) | -| size | global component size | ^[enum]`'large' \| 'default' \| 'small'` | default | -| zIndex | global Initial zIndex | ^[number] | — | -| namespace | global component className prefix (cooperated with [$namespace](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/config.scss#L1)) | ^[string] | el | -| button | button related configuration, [see the following table](#button-attribute) | ^[object]`{autoInsertSpace?: boolean}` | see the following table | -| message | message related configuration, [see the following table](#message-attribute) | ^[object]`{max?: number}` | see the following table | -| experimental-features | features at experimental stage to be added, all features are default to be set to false | ^[object] | — | +| Name | Description | Type | Default | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| +| locale | Locale Object | ^[object]`{name: string, el: TranslatePair}`[](https://github.com/element-plus/element-plus/blob/a98ff9b40c0c3d2b9959f99919bd8363e3e3c25a/packages/locale/index.ts#L5) [languages](https://github.com/element-plus/element-plus/tree/dev/packages/locale/lang) | [en](https://github.com/element-plus/element-plus/blob/dev/packages/locale/lang/en.ts) | +| size | global component size | ^[enum]`'large' \| 'default' \| 'small'` | default | +| zIndex | global Initial zIndex | ^[number] | — | +| namespace | global component className prefix (cooperated with [$namespace](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/config.scss#L1)) | ^[string] | el | +| button | button related configuration, [see the following table](#button-attribute) | ^[object]`{autoInsertSpace?: boolean}` | see the following table | +| message | message related configuration, [see the following table](#message-attribute) | ^[object]`{max?: number}` | see the following table | +| experimental-features | features at experimental stage to be added, all features are default to be set to false | ^[object] | — | +| empty-values ^(2.7.0) | global empty values of components | ^[array] | — | +| value-on-clear ^(2.7.0) | global clear return value | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ### Button Attribute | Attribute | Description | Type | Default | -| --------------- | ----------------------------------------------------------- | ---------- | ------- | +|-----------------|-------------------------------------------------------------|------------|---------| | autoInsertSpace | automatically insert a space between two chinese characters | ^[boolean] | false | ### Message Attribute | Attribute | Description | Type | Default | -| --------- | --------------------------------------------------------------------- | --------- | ------- | +|-----------|-----------------------------------------------------------------------|-----------|---------| | max | the maximum number of messages that can be displayed at the same time | ^[number] | — | ### Config Provider Slots | Name | Description | Scope | -| ------- | ------------------------- | ------------------------------------------------------- | +|---------|---------------------------|---------------------------------------------------------| | default | customize default content | config: provided global config (inherited from the top) | diff --git a/docs/en-US/component/date-picker.md b/docs/en-US/component/date-picker.md index 8480bb0a84..e297647bde 100644 --- a/docs/en-US/component/date-picker.md +++ b/docs/en-US/component/date-picker.md @@ -147,40 +147,42 @@ Note, date time locale (month name, first day of the week ...) are also configur ### Attributes -| Name | Description | Type | Default | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| model-value / v-model | binding value, if it is an array, the length should be 2 | ^[number] / ^[string] / ^[object]`Date \| [Date, Date] \| [string, string]` | '' | -| readonly | whether DatePicker is read only | ^[boolean] | false | -| disabled | whether DatePicker is disabled | ^[boolean] | false | -| size | size of Input | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | — | -| editable | whether the input is editable | ^[boolean] | true | -| clearable | whether to show clear button | ^[boolean] | true | -| placeholder | placeholder in non-range mode | ^[string] | '' | -| start-placeholder | placeholder for the start date in range mode | ^[string] | — | -| end-placeholder | placeholder for the end date in range mode | ^[string] | — | -| type | type of the picker | ^[enum]`'year' \| 'years' \|'month' \| 'date' \| 'dates' \| 'datetime' \| 'week' \| 'datetimerange' \| 'daterange' \| 'monthrange'` | date | -| format | format of the displayed value in the input box | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | YYYY-MM-DD | -| popper-class | custom class name for DatePicker's dropdown | ^[string] | — | -| popper-options | Customized popper option see more at [popper.js](https://popper.js.org/docs/v2/) | ^[object]`Partial` | {} | -| range-separator | range separator | ^[string] | '-' | -| default-value | optional, default date of the calendar | ^[object]`Date \| [Date, Date]` | — | -| default-time | optional, the time value to use when selecting date range | ^[object]`Date \| [Date, Date]` | — | -| value-format | optional, format of binding value. If not specified, the binding value will be a Date object | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | — | -| id | same as `id` in native input | ^[string] / ^[object]`[string, string]` | — | -| name | same as `name` in native input | ^[string] / ^[object]`[string, string]` | '' | -| unlink-panels | unlink two date-panels in range-picker | ^[boolean] | false | -| prefix-icon | custom prefix icon component. By default, if the value of `type` is `TimeLikeType`, the value is `Clock`, else is `Calendar` | ^[string] / ^[object]`Component` | '' | -| clear-icon | custom clear icon component | ^[string] / ^[object]`Component` | `CircleClose` | -| validate-event | whether to trigger form validation | ^[boolean] | true | -| disabled-date | a function determining if a date is disabled with that date as its parameter. Should return a Boolean | ^[Function]`(data: Date) => boolean` | — | -| shortcuts | an object array to set shortcut options | ^[object]`Array<{ text: string, value: Date \| Function }>` | [] | -| cell-class-name | set custom className | ^[Function]`(data: Date) => string` | — | -| teleported | whether date-picker dropdown is teleported to the body | ^[boolean] | true | +| Name | Description | Type | Default | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|---------------| +| model-value / v-model | binding value, if it is an array, the length should be 2 | ^[number] / ^[string] / ^[object]`Date \| [Date, Date] \| [string, string]` | '' | +| readonly | whether DatePicker is read only | ^[boolean] | false | +| disabled | whether DatePicker is disabled | ^[boolean] | false | +| size | size of Input | ^[enum]`'' \| 'large' \| 'default' \| 'small'` | — | +| editable | whether the input is editable | ^[boolean] | true | +| clearable | whether to show clear button | ^[boolean] | true | +| placeholder | placeholder in non-range mode | ^[string] | '' | +| start-placeholder | placeholder for the start date in range mode | ^[string] | — | +| end-placeholder | placeholder for the end date in range mode | ^[string] | — | +| type | type of the picker | ^[enum]`'year' \| 'years' \|'month' \| 'date' \| 'dates' \| 'datetime' \| 'week' \| 'datetimerange' \| 'daterange' \| 'monthrange'` | date | +| format | format of the displayed value in the input box | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | YYYY-MM-DD | +| popper-class | custom class name for DatePicker's dropdown | ^[string] | — | +| popper-options | Customized popper option see more at [popper.js](https://popper.js.org/docs/v2/) | ^[object]`Partial` | {} | +| range-separator | range separator | ^[string] | '-' | +| default-value | optional, default date of the calendar | ^[object]`Date \| [Date, Date]` | — | +| default-time | optional, the time value to use when selecting date range | ^[object]`Date \| [Date, Date]` | — | +| value-format | optional, format of binding value. If not specified, the binding value will be a Date object | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | — | +| id | same as `id` in native input | ^[string] / ^[object]`[string, string]` | — | +| name | same as `name` in native input | ^[string] / ^[object]`[string, string]` | '' | +| unlink-panels | unlink two date-panels in range-picker | ^[boolean] | false | +| prefix-icon | custom prefix icon component. By default, if the value of `type` is `TimeLikeType`, the value is `Clock`, else is `Calendar` | ^[string] / ^[object]`Component` | '' | +| clear-icon | custom clear icon component | ^[string] / ^[object]`Component` | `CircleClose` | +| validate-event | whether to trigger form validation | ^[boolean] | true | +| disabled-date | a function determining if a date is disabled with that date as its parameter. Should return a Boolean | ^[Function]`(data: Date) => boolean` | — | +| shortcuts | an object array to set shortcut options | ^[object]`Array<{ text: string, value: Date \| Function }>` | [] | +| cell-class-name | set custom className | ^[Function]`(data: Date) => string` | — | +| teleported | whether date-picker dropdown is teleported to the body | ^[boolean] | true | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ### Events | Name | Description | Type | -| --------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +|-----------------|------------------------------------------------------------|-------------------------------------------------------------------------------------------| | change | triggers when user confirms the value | ^[Function]`(val: typeof v-model) => void` | | blur | triggers when Input blurs | ^[Function]`(e: FocusEvent) => void` | | focus | triggers when Input focuses | ^[Function]`(e: FocusEvent) => void` | @@ -191,14 +193,14 @@ Note, date time locale (month name, first day of the week ...) are also configur ### Slots | Name | Description | -| --------------- | ------------------------------ | +|-----------------|--------------------------------| | default | custom cell content | | range-separator | custom range separator content | ### Exposes | Name | Description | Type | -| --------------------- | --------------------------- | ------------------------------------------------------------------------------ | +|-----------------------|-----------------------------|--------------------------------------------------------------------------------| | focus | focus the Input component | ^[Function]`(focusStartInput?: boolean, isIgnoreFocusEvent?: boolean) => void` | | handleOpen ^(2.2.16) | open the DatePicker popper | ^[Function]`() => void` | | handleClose ^(2.2.16) | close the DatePicker popper | ^[Function]`() => void` | diff --git a/docs/en-US/component/datetime-picker.md b/docs/en-US/component/datetime-picker.md index 82d1e4fda2..9d1b15bd92 100644 --- a/docs/en-US/component/datetime-picker.md +++ b/docs/en-US/component/datetime-picker.md @@ -75,41 +75,43 @@ datetime-picker/default-time ## Attributes -| Name | Description | Type | Accepted Values | Default | -| --------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------- | ------------------- | -| model-value / v-model | binding value, if it is an array, the length should be 2 | Date / number / string / Array | — | — | -| readonly | whether DatePicker is read only | boolean | — | false | -| disabled | whether DatePicker is disabled | boolean | — | false | -| editable | whether the input is editable | boolean | — | true | -| clearable | whether to show clear button | boolean | — | true | -| size | size of Input | string | large/default/small | default | -| placeholder | placeholder in non-range mode | string | — | — | -| start-placeholder | placeholder for the start date in range mode | string | — | — | -| end-placeholder | placeholder for the end date in range mode | string | — | — | -| arrow-control | whether to pick time using arrow buttons | boolean | — | false | -| type | type of the picker | string | year/month/date/datetime/ week/datetimerange/daterange | date | -| format | format of the displayed value in the input box | string | see [date formats](/en-US/component/date-picker#date-formats) | YYYY-MM-DD HH:mm:ss | -| popper-class | custom class name for DateTimePicker's dropdown | string | — | — | -| range-separator | range separator | string | — | '-' | -| default-value | optional, default date of the calendar | Date / [Date, Date] | | — | -| default-time | the default time value after picking a date. Time `00:00:00` will be used if not specified | Date / [Date, Date] | — | — | -| value-format | optional, format of binding value. If not specified, the binding value will be a Date object | string | see [date formats](https://day.js.org/docs/en/display/format) | — | -| date-format ^(2.4.0) | optional, format of the date displayed value in TimePicker's dropdown | string | see [date formats](https://day.js.org/docs/en/display/format) | — | -| time-format ^(2.4.0) | optional, format of the time displayed value in TimePicker's dropdown | string | see [date formats](https://day.js.org/docs/en/display/format) | — | -| id | same as `id` in native input | string / [string, string] | — | — | -| name | same as `name` in native input | string | — | — | -| unlink-panels | unlink two date-panels in range-picker | boolean | — | false | -| prefix-icon | Custom prefix icon component | `string \| Component` | — | Date | -| clear-icon | Custom clear icon component | `string \| Component` | — | CircleClose | -| shortcuts | an object array to set shortcut options | object[{ text: string, value: date / function }] | — | — | -| disabled-date | a function determining if a date is disabled with that date as its parameter. Should return a Boolean | function(Date) | — | — | -| cell-class-name | set custom className | Function(Date) | — | — | -| teleported | whether datetime-picker dropdown is teleported to the body | boolean | true / false | true | +| Name | Description | Type | Accepted Values | Default | +|-------------------------|----------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------------------|---------------------| +| model-value / v-model | binding value, if it is an array, the length should be 2 | Date / number / string / Array | — | — | +| readonly | whether DatePicker is read only | boolean | — | false | +| disabled | whether DatePicker is disabled | boolean | — | false | +| editable | whether the input is editable | boolean | — | true | +| clearable | whether to show clear button | boolean | — | true | +| size | size of Input | string | large/default/small | default | +| placeholder | placeholder in non-range mode | string | — | — | +| start-placeholder | placeholder for the start date in range mode | string | — | — | +| end-placeholder | placeholder for the end date in range mode | string | — | — | +| arrow-control | whether to pick time using arrow buttons | boolean | — | false | +| type | type of the picker | string | year/month/date/datetime/ week/datetimerange/daterange | date | +| format | format of the displayed value in the input box | string | see [date formats](/en-US/component/date-picker#date-formats) | YYYY-MM-DD HH:mm:ss | +| popper-class | custom class name for DateTimePicker's dropdown | string | — | — | +| range-separator | range separator | string | — | '-' | +| default-value | optional, default date of the calendar | Date / [Date, Date] | | — | +| default-time | the default time value after picking a date. Time `00:00:00` will be used if not specified | Date / [Date, Date] | — | — | +| value-format | optional, format of binding value. If not specified, the binding value will be a Date object | string | see [date formats](https://day.js.org/docs/en/display/format) | — | +| date-format ^(2.4.0) | optional, format of the date displayed value in TimePicker's dropdown | string | see [date formats](https://day.js.org/docs/en/display/format) | — | +| time-format ^(2.4.0) | optional, format of the time displayed value in TimePicker's dropdown | string | see [date formats](https://day.js.org/docs/en/display/format) | — | +| id | same as `id` in native input | string / [string, string] | — | — | +| name | same as `name` in native input | string | — | — | +| unlink-panels | unlink two date-panels in range-picker | boolean | — | false | +| prefix-icon | Custom prefix icon component | `string \| Component` | — | Date | +| clear-icon | Custom clear icon component | `string \| Component` | — | CircleClose | +| shortcuts | an object array to set shortcut options | object[{ text: string, value: date / function }] | — | — | +| disabled-date | a function determining if a date is disabled with that date as its parameter. Should return a Boolean | function(Date) | — | — | +| cell-class-name | set custom className | Function(Date) | — | — | +| teleported | whether datetime-picker dropdown is teleported to the body | boolean | true / false | true | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ## Events | Name | Description | Parameters | -| --------------- | ----------------------------------------------------------------------------- | ----------------------------------------- | +|-----------------|-------------------------------------------------------------------------------|-------------------------------------------| | change | triggers when user confirms the value | component's binding value | | blur | triggers when Input blurs | `(e: FocusEvent)` | | focus | triggers when Input focuses | `(e: FocusEvent)` | @@ -119,12 +121,12 @@ datetime-picker/default-time ## Methods | Method | Description | Parameters | -| ------ | ------------------------- | ---------- | +|--------|---------------------------|------------| | focus | focus the Input component | — | ## Slots | Name | Description | -| --------------- | ------------------------------ | +|-----------------|--------------------------------| | default | custom cell content | | range-separator | custom range separator content | diff --git a/docs/en-US/component/select-v2.md b/docs/en-US/component/select-v2.md index ccd13b7d07..383520029a 100644 --- a/docs/en-US/component/select-v2.md +++ b/docs/en-US/component/select-v2.md @@ -197,12 +197,24 @@ select-v2/custom-loading ::: +## Empty Values ^(2.7.0) + +If you want to support empty string, please set `empty-values` to `[null, undefined]`. + +If you want to change the clear value to `null`, please set `value-on-clear` to `null`. + +:::demo + +select-v2/empty-values + +::: + ## API ### Attributes | Name | Description | Type | Default | -| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +|-------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| | model-value / v-model | binding value | ^[string] / ^[number] / ^[boolean] / ^[object] / ^[array] | — | | options | data of the options, the key of `value` and `label` can be customize by `props` | ^[array] | — | | props ^(2.4.2) | configuration options, see the following table | ^[object] | — | @@ -244,11 +256,13 @@ select-v2/custom-loading | max-collapse-tags ^(2.3.0) | The max tags number to be shown. To use this, `collapse-tags` must be true | ^[number] | 1 | | tag-type ^(2.5.0) | tag type | ^[enum]`'' \| 'success' \| 'info' \| 'warning' \| 'danger'` | info | | aria-label ^(a11y) ^(2.5.0) | same as `aria-label` in native input | ^[string] | — | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ### props | Attribute | Description | Type | Default | -| --------- | --------------------------------------------------------------- | --------- | -------- | +|-----------|-----------------------------------------------------------------|-----------|----------| | value | specify which key of node object is used as the node's value | ^[string] | value | | label | specify which key of node object is used as the node's label | ^[string] | label | | options | specify which key of node object is used as the node's children | ^[string] | options | @@ -257,7 +271,7 @@ select-v2/custom-loading ### Events | Name | Description | Type | -| -------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +|----------------|------------------------------------------------------------------------------------------------------------|------------------------------------------| | change | triggers when the selected value changes, the param is current selected value | ^[Function]`(val: any) => void` | | visible-change | triggers when the dropdown appears/disappears, the param will be true when it appears, and false otherwise | ^[Function]`(visible: boolean) => void` | | remove-tag | triggers when a tag is removed in multiple mode, the param is removed tag value | ^[Function]`(tagValue: any) => void` | @@ -268,7 +282,7 @@ select-v2/custom-loading ### Slots | Name | Description | -| ---------------- | ------------------------------------- | +|------------------|---------------------------------------| | default | Option renderer | | header ^(2.5.2) | content at the top of the dropdown | | footer ^(2.5.2) | content at the bottom of the dropdown | @@ -280,6 +294,6 @@ select-v2/custom-loading ### Exposes | Method | Description | Type | -| ------ | ----------------------------------------------- | ----------------------- | +|--------|-------------------------------------------------|-------------------------| | focus | focus the Input component | ^[Function]`() => void` | | blur | blur the Input component, and hide the dropdown | ^[Function]`() => void` | diff --git a/docs/en-US/component/select.md b/docs/en-US/component/select.md index a4aaddac83..82bd933f8b 100644 --- a/docs/en-US/component/select.md +++ b/docs/en-US/component/select.md @@ -165,12 +165,24 @@ select/custom-loading ::: +## Empty Values ^(2.7.0) + +If you want to support empty string, please set `empty-values` to `[null, undefined]`. + +If you want to change the clear value to `null`, please set `value-on-clear` to `null`. + +:::demo + +select/empty-values + +::: + ## Select API ### Select Attributes | Name | Description | Type | Default | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | +|---------------------------------|-----------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| | model-value / v-model | binding value | ^[string] / ^[number] / ^[boolean] / ^[object] / ^[array] | — | | multiple | whether multiple-select is activated | ^[boolean] | false | | disabled | whether Select is disabled | ^[boolean] | false | @@ -211,6 +223,8 @@ select/custom-loading | max-collapse-tags ^(2.3.0) | the max tags number to be shown. To use this, `collapse-tags` must be true | ^[number] | 1 | | popper-options | [popper.js](https://popper.js.org/docs/v2/) parameters | ^[object]refer to [popper.js](https://popper.js.org/docs/v2/) doc | {} | | aria-label ^(a11y) | same as `aria-label` in native input | ^[string] | — | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | :::warning @@ -221,7 +235,7 @@ select/custom-loading ### Select Events | Name | Description | Type | -| -------------- | ------------------------------------------------------------- | ---------------------------------------- | +|----------------|---------------------------------------------------------------|------------------------------------------| | change | triggers when the selected value changes | ^[Function]`(value: any) => void` | | visible-change | triggers when the dropdown appears/disappears | ^[Function]`(visible: boolean) => void` | | remove-tag | triggers when a tag is removed in multiple mode | ^[Function]`(tagValue: any) => void` | @@ -232,7 +246,7 @@ select/custom-loading ### Select Slots | Name | Description | Subtags | -| ---------------- | ------------------------------------- | --------------------- | +|------------------|---------------------------------------|-----------------------| | default | option component list | Option Group / Option | | header ^(2.4.3) | content at the top of the dropdown | — | | footer ^(2.4.3) | content at the bottom of the dropdown | — | @@ -244,7 +258,7 @@ select/custom-loading ### Select Exposes | Method | Description | Type | -| ------ | ----------------------------------------------- | ----------------------- | +|--------|-------------------------------------------------|-------------------------| | focus | focus the Input component | ^[Function]`() => void` | | blur | blur the Input component, and hide the dropdown | ^[Function]`() => void` | @@ -253,14 +267,14 @@ select/custom-loading ### Option Group Attributes | Name | Description | Type | Default | -| -------- | -------------------------------------------- | ---------- | ------- | +|----------|----------------------------------------------|------------|---------| | label | name of the group | ^[string] | — | | disabled | whether to disable all options in this group | ^[boolean] | false | ### Option Group Slots | Name | Description | Subtags | -| ------- | ------------------------- | ------- | +|---------|---------------------------|---------| | default | customize default content | Option | ## Option API @@ -268,7 +282,7 @@ select/custom-loading ### Option Attributes | Name | Description | Type | Default | -| -------- | ------------------------------------------- | ---------------------------------------------- | ------- | +|----------|---------------------------------------------|------------------------------------------------|---------| | value | value of option | ^[string] / ^[number] / ^[boolean] / ^[object] | — | | label | label of option, same as `value` if omitted | ^[string] / ^[number] | — | | disabled | whether option is disabled | ^[boolean] | false | @@ -276,5 +290,5 @@ select/custom-loading ### Option Slots | Name | Description | -| ------- | ------------------------- | +|---------|---------------------------| | default | customize default content | diff --git a/docs/en-US/component/time-picker.md b/docs/en-US/component/time-picker.md index 6cde162615..76cd60b6ec 100644 --- a/docs/en-US/component/time-picker.md +++ b/docs/en-US/component/time-picker.md @@ -47,39 +47,41 @@ time-picker/range ### Attributes -| Name | Description | Type | Default | -| --------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ----------- | -| model-value / v-model | binding value, if it is an array, the length should be 2 | ^[number] / ^[string] / ^[object]`Date \| [Date, Date] \| [number, number] \| [string, string]` | '' | -| readonly | whether TimePicker is read only | ^[boolean] | false | -| disabled | whether TimePicker is disabled | ^[boolean] | false | -| editable | whether the input is editable | ^[boolean] | true | -| clearable | whether to show clear button | ^[boolean] | true | -| size | size of Input | ^[enum]`'large' \| 'default' \| 'small'` | — | -| placeholder | placeholder in non-range mode | ^[string] | '' | -| start-placeholder | placeholder for the start time in range mode | ^[string] | — | -| end-placeholder | placeholder for the end time in range mode | ^[string] | — | -| is-range | whether to pick a time range | ^[boolean] | false | -| arrow-control | whether to pick time using arrow buttons | ^[boolean] | false | -| popper-class | custom class name for TimePicker's dropdown | ^[string] | '' | -| range-separator | range separator | ^[string] | '-' | -| format | format of the displayed value in the input box | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | — | -| default-value | optional, default date of the calendar | ^[Date] / ^[object]`[Date, Date]` | — | -| value-format | optional, format of binding value. If not specified, the binding value will be a Date object | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | — | -| id | same as `id` in native input | ^[string] / ^[object]`[string, string]` | — | -| name | same as `name` in native input | ^[string] | '' | -| label ^(a11y) | same as `aria-label` in native input | ^[string] | — | -| prefix-icon | Custom prefix icon component | ^[string] / ^[Component] | Clock | -| clear-icon | Custom clear icon component | ^[string] / ^[Component] | CircleClose | -| disabled-hours | To specify the array of hours that cannot be selected | ^[Function]`(role: string, comparingDate?: Dayjs) => number[]` | — | -| disabled-minutes | To specify the array of minutes that cannot be selected | ^[Function]`(hour: number, role: string, comparingDate?: Dayjs) => number[]` | — | -| disabled-seconds | To specify the array of seconds that cannot be selected | ^[Function]`(hour: number, minute: number, role: string, comparingDate?: Dayjs) => number[]` | — | -| teleported | whether time-picker dropdown is teleported to the body | ^[boolean] | true | -| tabindex | input tabindex | ^[string] / ^[number] | 0 | +| Name | Description | Type | Default | +|-------------------------|----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|-------------| +| model-value / v-model | binding value, if it is an array, the length should be 2 | ^[number] / ^[string] / ^[object]`Date \| [Date, Date] \| [number, number] \| [string, string]` | '' | +| readonly | whether TimePicker is read only | ^[boolean] | false | +| disabled | whether TimePicker is disabled | ^[boolean] | false | +| editable | whether the input is editable | ^[boolean] | true | +| clearable | whether to show clear button | ^[boolean] | true | +| size | size of Input | ^[enum]`'large' \| 'default' \| 'small'` | — | +| placeholder | placeholder in non-range mode | ^[string] | '' | +| start-placeholder | placeholder for the start time in range mode | ^[string] | — | +| end-placeholder | placeholder for the end time in range mode | ^[string] | — | +| is-range | whether to pick a time range | ^[boolean] | false | +| arrow-control | whether to pick time using arrow buttons | ^[boolean] | false | +| popper-class | custom class name for TimePicker's dropdown | ^[string] | '' | +| range-separator | range separator | ^[string] | '-' | +| format | format of the displayed value in the input box | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | — | +| default-value | optional, default date of the calendar | ^[Date] / ^[object]`[Date, Date]` | — | +| value-format | optional, format of binding value. If not specified, the binding value will be a Date object | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | — | +| id | same as `id` in native input | ^[string] / ^[object]`[string, string]` | — | +| name | same as `name` in native input | ^[string] | '' | +| label ^(a11y) | same as `aria-label` in native input | ^[string] | — | +| prefix-icon | Custom prefix icon component | ^[string] / ^[Component] | Clock | +| clear-icon | Custom clear icon component | ^[string] / ^[Component] | CircleClose | +| disabled-hours | To specify the array of hours that cannot be selected | ^[Function]`(role: string, comparingDate?: Dayjs) => number[]` | — | +| disabled-minutes | To specify the array of minutes that cannot be selected | ^[Function]`(hour: number, role: string, comparingDate?: Dayjs) => number[]` | — | +| disabled-seconds | To specify the array of seconds that cannot be selected | ^[Function]`(hour: number, minute: number, role: string, comparingDate?: Dayjs) => number[]` | — | +| teleported | whether time-picker dropdown is teleported to the body | ^[boolean] | true | +| tabindex | input tabindex | ^[string] / ^[number] | 0 | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ### Events | Name | Description | Type | -| -------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +|----------------|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| | change | triggers when user confirms the value | ^[Function]`(val: number \| string \| Date \| [number, number] \| [string, string] \| [Date, Date]) => void` | | blur | triggers when Input blurs | ^[Function]`(e: FocusEvent) => void` | | focus | triggers when Input focuses | ^[Function]`(e: FocusEvent) => void` | @@ -88,7 +90,7 @@ time-picker/range ### Exposes | Name | Description | Type | -| --------------------- | --------------------------- | ------------------------------------------------- | +|-----------------------|-----------------------------|---------------------------------------------------| | focus | focus the Input component | ^[Function]`(e: FocusEvent \| undefined) => void` | | blur | blur the Input component | ^[Function]`(e: FocusEvent \| undefined) => void` | | handleOpen ^(2.2.16) | open the TimePicker popper | ^[Function]`() => void` | diff --git a/docs/en-US/component/time-select.md b/docs/en-US/component/time-select.md index 3f3c7c6aba..e5b7ef27cd 100644 --- a/docs/en-US/component/time-select.md +++ b/docs/en-US/component/time-select.md @@ -57,29 +57,31 @@ time-select/time-range ### Attributes -| Name | Description | Type | Default | -| --------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------- | -| model-value / v-model | binding value | ^[string] | — | -| disabled | whether TimeSelect is disabled | ^[boolean] | false | -| editable | whether the input is editable | ^[boolean] | true | -| clearable | whether to show clear button | ^[boolean] | true | -| size | size of Input | ^[enum]`'large' \| 'default' \| 'small'` | default | -| placeholder | placeholder in non-range mode | ^[string] | — | -| name | same as `name` in native input | ^[string] | — | -| effect | Tooltip theme, built-in theme: `dark` / `light` | ^[string] / ^[enum]`'dark' \| 'light'` | light | -| prefix-icon | custom prefix icon component | ^[string] / ^[Component] | Clock | -| clear-icon | custom clear icon component | ^[string] / ^[Component] | CircleClose | -| start | start time | ^[string] | 09:00 | -| end | end time | ^[string] | 18:00 | -| step | time step | ^[string] | 00:30 | -| min-time | minimum time, any time before this time will be disabled | ^[string] | — | -| max-time | maximum time, any time after this time will be disabled | ^[string] | — | -| format | set format of time | ^[string] see [formats](https://day.js.org/docs/en/display/format#list-of-all-available-formats) | HH:mm | +| Name | Description | Type | Default | +|-------------------------|----------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|-------------| +| model-value / v-model | binding value | ^[string] | — | +| disabled | whether TimeSelect is disabled | ^[boolean] | false | +| editable | whether the input is editable | ^[boolean] | true | +| clearable | whether to show clear button | ^[boolean] | true | +| size | size of Input | ^[enum]`'large' \| 'default' \| 'small'` | default | +| placeholder | placeholder in non-range mode | ^[string] | — | +| name | same as `name` in native input | ^[string] | — | +| effect | Tooltip theme, built-in theme: `dark` / `light` | ^[string] / ^[enum]`'dark' \| 'light'` | light | +| prefix-icon | custom prefix icon component | ^[string] / ^[Component] | Clock | +| clear-icon | custom clear icon component | ^[string] / ^[Component] | CircleClose | +| start | start time | ^[string] | 09:00 | +| end | end time | ^[string] | 18:00 | +| step | time step | ^[string] | 00:30 | +| min-time | minimum time, any time before this time will be disabled | ^[string] | — | +| max-time | maximum time, any time after this time will be disabled | ^[string] | — | +| format | set format of time | ^[string] see [formats](https://day.js.org/docs/en/display/format#list-of-all-available-formats) | HH:mm | +| empty-values ^(2.7.0) | empty values of component, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[array] | — | +| value-on-clear ^(2.7.0) | clear return value, [see config-provider](/en-US/component/config-provider#empty-values-configurations) | ^[string] / ^[number] / ^[boolean] / ^[Function] | — | ### Events | Name | Description | Type | -| ------ | ------------------------------------- | ---------------------------------------- | +|--------|---------------------------------------|------------------------------------------| | change | triggers when user confirms the value | ^[Function]`(value: string) => void` | | blur | triggers when Input blurs | ^[Function]`(event: FocusEvent) => void` | | focus | triggers when Input focuses | ^[Function]`(event: FocusEvent) => void` | @@ -87,6 +89,6 @@ time-select/time-range ### Exposes | Method | Description | Type | -| ------ | ------------------------- | ----------------------- | +|--------|---------------------------|-------------------------| | focus | focus the Input component | ^[Function]`() => void` | | blur | blur the Input component | ^[Function]`() => void` | diff --git a/docs/examples/config-provider/empty-values.vue b/docs/examples/config-provider/empty-values.vue new file mode 100644 index 0000000000..b126ae7de4 --- /dev/null +++ b/docs/examples/config-provider/empty-values.vue @@ -0,0 +1,69 @@ + + + diff --git a/docs/examples/select-v2/empty-values.vue b/docs/examples/select-v2/empty-values.vue new file mode 100644 index 0000000000..52382e9626 --- /dev/null +++ b/docs/examples/select-v2/empty-values.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/examples/select/empty-values.vue b/docs/examples/select/empty-values.vue new file mode 100644 index 0000000000..f0bc2f4ea1 --- /dev/null +++ b/docs/examples/select/empty-values.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/components/cascader/src/cascader.ts b/packages/components/cascader/src/cascader.ts index a5ff67ab47..9f7f0ac54e 100644 --- a/packages/components/cascader/src/cascader.ts +++ b/packages/components/cascader/src/cascader.ts @@ -1,6 +1,6 @@ import { CommonProps } from '@element-plus/components/cascader-panel' import { buildProps, definePropType, isBoolean } from '@element-plus/utils' -import { useSizeProp } from '@element-plus/hooks' +import { useEmptyValuesProps, useSizeProp } from '@element-plus/hooks' import { useTooltipContentProps } from '@element-plus/components/tooltip' import { tagProps } from '@element-plus/components/tag' import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants' @@ -110,11 +110,14 @@ export const cascaderProps = buildProps({ type: Boolean, default: true, }, + ...useEmptyValuesProps, }) export const cascaderEmits = { - [UPDATE_MODEL_EVENT]: (val: CascaderValue) => !!val || val === null, - [CHANGE_EVENT]: (val: CascaderValue) => !!val || val === null, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [UPDATE_MODEL_EVENT]: (_: CascaderValue) => true, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + [CHANGE_EVENT]: (_: CascaderValue) => true, focus: (evt: FocusEvent) => evt instanceof FocusEvent, blur: (evt: FocusEvent) => evt instanceof FocusEvent, visibleChange: (val: boolean) => isBoolean(val), diff --git a/packages/components/cascader/src/cascader.vue b/packages/components/cascader/src/cascader.vue index d684cfc601..31ef7455ad 100644 --- a/packages/components/cascader/src/cascader.vue +++ b/packages/components/cascader/src/cascader.vue @@ -207,7 +207,7 @@ import ElTag from '@element-plus/components/tag' import ElIcon from '@element-plus/components/icon' import { useFormItem, useFormSize } from '@element-plus/components/form' import { ClickOutside as vClickoutside } from '@element-plus/directives' -import { useLocale, useNamespace } from '@element-plus/hooks' +import { useEmptyValues, useLocale, useNamespace } from '@element-plus/hooks' import { debugWarn, focusNode, @@ -268,6 +268,7 @@ const nsInput = useNamespace('input') const { t } = useLocale() const { form, formItem } = useFormItem() +const { valueOnClear } = useEmptyValues(props) const tooltipRef: Ref = ref(null) const input: Ref = ref(null) @@ -340,8 +341,9 @@ const checkedValue = computed({ return cloneDeep(props.modelValue) as CascaderValue }, set(val) { - emit(UPDATE_MODEL_EVENT, val) - emit(CHANGE_EVENT, val) + const value = val || valueOnClear.value + emit(UPDATE_MODEL_EVENT, value) + emit(CHANGE_EVENT, value) if (props.validateEvent) { formItem?.validate('change').catch((err) => debugWarn(err)) } diff --git a/packages/components/config-provider/src/config-provider-props.ts b/packages/components/config-provider/src/config-provider-props.ts index 6e2d0ad9df..c0e4890847 100644 --- a/packages/components/config-provider/src/config-provider-props.ts +++ b/packages/components/config-provider/src/config-provider-props.ts @@ -1,5 +1,5 @@ import { buildProps, definePropType } from '@element-plus/utils' -import { useSizeProp } from '@element-plus/hooks' +import { useEmptyValuesProps, useSizeProp } from '@element-plus/hooks' import type { ExtractPropTypes } from 'vue' import type { Language } from '@element-plus/locale' @@ -64,5 +64,6 @@ export const configProviderProps = buildProps({ type: String, default: 'el', }, + ...useEmptyValuesProps, } as const) export type ConfigProviderProps = ExtractPropTypes diff --git a/packages/components/config-provider/src/hooks/use-global-config.ts b/packages/components/config-provider/src/hooks/use-global-config.ts index fa8c6d05c0..87a0231d90 100644 --- a/packages/components/config-provider/src/hooks/use-global-config.ts +++ b/packages/components/config-provider/src/hooks/use-global-config.ts @@ -124,7 +124,7 @@ const mergeConfig = ( const keys = [...new Set([...keysOf(a), ...keysOf(b)])] const obj: Record = {} for (const key of keys) { - obj[key] = b[key] ?? a[key] + obj[key] = b[key] !== undefined ? b[key] : a[key] } return obj } diff --git a/packages/components/date-picker/__tests__/date-picker.test.ts b/packages/components/date-picker/__tests__/date-picker.test.ts index 1e41efd4c1..c1c87576d9 100644 --- a/packages/components/date-picker/__tests__/date-picker.test.ts +++ b/packages/components/date-picker/__tests__/date-picker.test.ts @@ -178,7 +178,7 @@ describe('DatePicker', () => { ;(picker.vm as any).showClose = true await nextTick() ;(document.querySelector('.clear-icon') as HTMLElement).click() - expect(vm.value).toBeNull() + expect(vm.value).toBe(null) }) it('defaultValue', async () => { @@ -207,7 +207,7 @@ describe('DatePicker', () => { ;(picker.vm as any).showClose = true await nextTick() document.querySelector('.clear-icon').click() - expect(vm.value).toBeNull() + expect(vm.value).toBe(null) vm.defaultValue = new Date(2031, 5, 1) input.trigger('blur') diff --git a/packages/components/select-v2/src/defaults.ts b/packages/components/select-v2/src/defaults.ts index 5f22ebc85f..ed02bb093d 100644 --- a/packages/components/select-v2/src/defaults.ts +++ b/packages/components/select-v2/src/defaults.ts @@ -1,5 +1,5 @@ import { placements } from '@popperjs/core' -import { useSizeProp } from '@element-plus/hooks' +import { useEmptyValuesProps, useSizeProp } from '@element-plus/hooks' import { buildProps, definePropType, iconPropType } from '@element-plus/utils' import { useTooltipContentProps } from '@element-plus/components/tooltip' import { CircleClose } from '@element-plus/icons-vue' @@ -247,6 +247,7 @@ export const SelectProps = buildProps({ type: String, default: undefined, }, + ...useEmptyValuesProps, } as const) export const OptionProps = buildProps({ diff --git a/packages/components/select-v2/src/useSelect.ts b/packages/components/select-v2/src/useSelect.ts index 2d7c402e8d..ecb7d82bc0 100644 --- a/packages/components/select-v2/src/useSelect.ts +++ b/packages/components/select-v2/src/useSelect.ts @@ -13,11 +13,11 @@ import { findLastIndex, get, isEqual, - isNil, debounce as lodashDebounce, } from 'lodash-unified' import { useResizeObserver } from '@vueuse/core' import { + useEmptyValues, useFocusController, useLocale, useNamespace, @@ -59,6 +59,7 @@ const useSelect = (props: ISelectV2Props, emit) => { formItemContext: elFormItem, }) const { getLabel, getValue, getDisabled, getOptions } = useProps(props) + const { valueOnClear, isEmptyValue } = useEmptyValues(props) const states = reactive({ inputValue: '', @@ -127,24 +128,19 @@ const useSelect = (props: ISelectV2Props, emit) => { return totalHeight > props.height ? props.height : totalHeight }) - const hasEmptyStringOption = computed(() => - allOptions.value.some((option) => getValue(option) === '') - ) - const hasModelValue = computed(() => { return props.multiple ? isArray(props.modelValue) && props.modelValue.length > 0 - : !isNil(props.modelValue) && - (props.modelValue !== '' || hasEmptyStringOption.value) + : !isEmptyValue(props.modelValue) }) const showClearBtn = computed(() => { - const criteria = + return ( props.clearable && !selectDisabled.value && states.inputHovering && hasModelValue.value - return criteria + ) }) const iconComponent = computed(() => @@ -600,7 +596,7 @@ const useSelect = (props: ISelectV2Props, emit) => { if (isArray(props.modelValue)) { emptyValue = [] } else { - emptyValue = undefined + emptyValue = valueOnClear.value } if (props.multiple) { diff --git a/packages/components/select/src/select.ts b/packages/components/select/src/select.ts index ca1b8ce2db..a8085690c9 100644 --- a/packages/components/select/src/select.ts +++ b/packages/components/select/src/select.ts @@ -1,5 +1,5 @@ import { placements } from '@popperjs/core' -import { useSizeProp } from '@element-plus/hooks' +import { useEmptyValuesProps, useSizeProp } from '@element-plus/hooks' import { buildProps, definePropType, iconPropType } from '@element-plus/utils' import { useTooltipContentProps } from '@element-plus/components/tooltip' import { ArrowDown, CircleClose } from '@element-plus/icons-vue' @@ -219,4 +219,5 @@ export const SelectProps = buildProps({ type: String, default: undefined, }, + ...useEmptyValuesProps, }) diff --git a/packages/components/select/src/useSelect.ts b/packages/components/select/src/useSelect.ts index dc09df258c..8dc2dc32b4 100644 --- a/packages/components/select/src/useSelect.ts +++ b/packages/components/select/src/useSelect.ts @@ -14,7 +14,6 @@ import { findLastIndex, get, isEqual, - isNil, debounce as lodashDebounce, } from 'lodash-unified' import { useResizeObserver } from '@vueuse/core' @@ -33,6 +32,7 @@ import { scrollIntoView, } from '@element-plus/utils' import { + useEmptyValues, useFocusController, useId, useLocale, @@ -120,27 +120,23 @@ export const useSelect = (props: ISelectProps, emit) => { const { inputId } = useFormItemInputId(props, { formItemContext: formItem, }) + const { valueOnClear, isEmptyValue } = useEmptyValues(props) const selectDisabled = computed(() => props.disabled || form?.disabled) - const hasEmptyStringOption = computed(() => - optionsArray.value.some((option) => option.value === '') - ) - const hasModelValue = computed(() => { return props.multiple ? isArray(props.modelValue) && props.modelValue.length > 0 - : !isNil(props.modelValue) && - (props.modelValue !== '' || hasEmptyStringOption.value) + : !isEmptyValue(props.modelValue) }) const showClose = computed(() => { - const criteria = + return ( props.clearable && !selectDisabled.value && states.inputHovering && hasModelValue.value - return criteria + ) }) const iconComponent = computed(() => props.remote && props.filterable && !props.remoteShowSuffix @@ -527,7 +523,7 @@ export const useSelect = (props: ISelectProps, emit) => { const deleteSelected = (event) => { event.stopPropagation() - const value: string | any[] = props.multiple ? [] : undefined + const value: string | any[] = props.multiple ? [] : valueOnClear.value if (props.multiple) { for (const item of states.selected) { if (item.isDisabled) value.push(item.value) diff --git a/packages/components/time-picker/src/common/picker.vue b/packages/components/time-picker/src/common/picker.vue index 09be004938..eade05ef9e 100644 --- a/packages/components/time-picker/src/common/picker.vue +++ b/packages/components/time-picker/src/common/picker.vue @@ -173,7 +173,7 @@ import { } from 'vue' import { isEqual } from 'lodash-unified' import { onClickOutside } from '@vueuse/core' -import { useLocale, useNamespace } from '@element-plus/hooks' +import { useEmptyValues, useLocale, useNamespace } from '@element-plus/hooks' import { useFormItem, useFormSize } from '@element-plus/components/form' import ElInput from '@element-plus/components/input' import ElIcon from '@element-plus/components/icon' @@ -225,6 +225,7 @@ const nsRange = useNamespace('range') const { form, formItem } = useFormItem() const elPopperOptions = inject('ElPopperOptions', {} as Options) +const { valueOnClear } = useEmptyValues(props, null) const refPopper = ref() const inputRef = ref() @@ -502,8 +503,8 @@ const onClearIconClick = (event: MouseEvent) => { if (showClose.value) { event.stopPropagation() focusOnInputBox() - emitInput(null) - emitChange(null, true) + emitInput(valueOnClear.value) + emitChange(valueOnClear.value, true) showClose.value = false pickerVisible.value = false pickerOptions.value.handleClear && pickerOptions.value.handleClear() @@ -590,8 +591,8 @@ const handleChange = () => { } } if (userInput.value === '') { - emitInput(null) - emitChange(null) + emitInput(valueOnClear.value) + emitChange(valueOnClear.value) userInput.value = null } } diff --git a/packages/components/time-picker/src/common/props.ts b/packages/components/time-picker/src/common/props.ts index 97bc0de42c..ed442138f9 100644 --- a/packages/components/time-picker/src/common/props.ts +++ b/packages/components/time-picker/src/common/props.ts @@ -1,5 +1,5 @@ import { buildProps, definePropType } from '@element-plus/utils' -import { useSizeProp } from '@element-plus/hooks' +import { useEmptyValuesProps, useSizeProp } from '@element-plus/hooks' import { CircleClose } from '@element-plus/icons-vue' import { disabledTimeListsProps } from '../props/shared' @@ -211,6 +211,7 @@ export const timePickerDefaultProps = buildProps({ * @description unlink two date-panels in range-picker */ unlinkPanels: Boolean, + ...useEmptyValuesProps, } as const) export type TimePickerDefaultProps = ExtractPropTypes< diff --git a/packages/components/time-select/src/time-select.ts b/packages/components/time-select/src/time-select.ts index 67b62c575f..499f0b333d 100644 --- a/packages/components/time-select/src/time-select.ts +++ b/packages/components/time-select/src/time-select.ts @@ -1,6 +1,6 @@ import { buildProps, definePropType } from '@element-plus/utils' import { CircleClose, Clock } from '@element-plus/icons-vue' -import { useSizeProp } from '@element-plus/hooks' +import { useEmptyValuesProps, useSizeProp } from '@element-plus/hooks' import type TimeSelect from './time-select.vue' import type { Component, ExtractPropTypes, PropType } from 'vue' @@ -96,6 +96,7 @@ export const timeSelectProps = buildProps({ type: definePropType([String, Object]), default: () => CircleClose, }, + ...useEmptyValuesProps, } as const) export type TimeSelectProps = ExtractPropTypes diff --git a/packages/components/time-select/src/time-select.vue b/packages/components/time-select/src/time-select.vue index a4bff1cc63..9e9a2b5425 100644 --- a/packages/components/time-select/src/time-select.vue +++ b/packages/components/time-select/src/time-select.vue @@ -10,6 +10,8 @@ :placeholder="placeholder" default-first-option :filterable="editable" + :empty-values="emptyValues" + :value-on-clear="valueOnClear" @update:model-value="(event) => $emit('update:modelValue', event)" @change="(event) => $emit('change', event)" @blur="(event) => $emit('blur', event)" diff --git a/packages/hooks/__tests__/use-empty-values.test.tsx b/packages/hooks/__tests__/use-empty-values.test.tsx new file mode 100644 index 0000000000..2fbce5c791 --- /dev/null +++ b/packages/hooks/__tests__/use-empty-values.test.tsx @@ -0,0 +1,81 @@ +import { reactive } from 'vue' +import { describe, expect, it } from 'vitest' +import { + DEFAULT_EMPTY_VALUES, + DEFAULT_VALUE_ON_CLEAR, + useEmptyValues, +} from '../use-empty-values' + +describe('useEmptyValues', () => { + it('should return default value', async () => { + const props = reactive({}) as any + + const { emptyValues, valueOnClear } = useEmptyValues(props) + + expect(emptyValues.value).toEqual(DEFAULT_EMPTY_VALUES) + expect(valueOnClear.value).toEqual(DEFAULT_VALUE_ON_CLEAR) + }) + + it('should return default value prop', async () => { + const props = reactive({}) as any + + const { valueOnClear } = useEmptyValues(props, null) + + expect(valueOnClear.value).toEqual(null) + }) + + it('should return props value', async () => { + const props = reactive({ + emptyValues: [null, undefined], + valueOnClear: null, + }) as any + + const { emptyValues, valueOnClear } = useEmptyValues(props) + + expect(emptyValues.value).toEqual([null, undefined]) + expect(valueOnClear.value).toEqual(null) + + props.emptyValues = [''] + props.valueOnClear = '' + + expect(emptyValues.value).toEqual(['']) + expect(valueOnClear.value).toEqual('') + + props.emptyValues = [null] + props.valueOnClear = null + + expect(emptyValues.value).toEqual([null]) + expect(valueOnClear.value).toEqual(null) + + props.emptyValues = [undefined] + props.valueOnClear = () => undefined + + expect(emptyValues.value).toEqual([undefined]) + expect(valueOnClear.value).toEqual(undefined) + }) + + it('should judge empty value', async () => { + const props = reactive({}) as any + + const { isEmptyValue } = useEmptyValues(props) + + expect(isEmptyValue('')).toBe(true) + expect(isEmptyValue(undefined)).toBe(true) + expect(isEmptyValue(null)).toBe(true) + expect(isEmptyValue('null')).toBe(false) + expect(isEmptyValue(Number.NaN)).toBe(false) + expect(isEmptyValue(false)).toBe(false) + expect(isEmptyValue(0)).toBe(false) + + props.emptyValues = ['', Number.NaN, 'null', false, 0] + props.valueOnClear = '' + + expect(isEmptyValue('')).toBe(true) + expect(isEmptyValue(undefined)).toBe(false) + expect(isEmptyValue(null)).toBe(false) + expect(isEmptyValue('null')).toBe(true) + expect(isEmptyValue(Number.NaN)).toBe(true) + expect(isEmptyValue(false)).toBe(true) + expect(isEmptyValue(0)).toBe(true) + }) +}) diff --git a/packages/hooks/index.ts b/packages/hooks/index.ts index 1ae3a08795..0712d5aa37 100644 --- a/packages/hooks/index.ts +++ b/packages/hooks/index.ts @@ -27,3 +27,4 @@ export * from './use-cursor' export * from './use-ordered-children' export * from './use-size' export * from './use-focus-controller' +export * from './use-empty-values' diff --git a/packages/hooks/use-empty-values/index.ts b/packages/hooks/use-empty-values/index.ts new file mode 100644 index 0000000000..0818716cd5 --- /dev/null +++ b/packages/hooks/use-empty-values/index.ts @@ -0,0 +1,64 @@ +import { computed } from 'vue' +import { useGlobalConfig } from '@element-plus/components/config-provider' +import { buildProps, debugWarn, isFunction } from '@element-plus/utils' + +import type { ExtractPropTypes } from 'vue' + +export const SCOPE = 'use-empty-values' +export const DEFAULT_EMPTY_VALUES = ['', undefined, null] +export const DEFAULT_VALUE_ON_CLEAR = undefined + +export const useEmptyValuesProps = buildProps({ + /** + * @description empty values supported by the component + */ + emptyValues: Array, + /** + * @description return value when cleared, if you want to set `undefined`, use `() => undefined` + */ + valueOnClear: { + type: [String, Number, Boolean, Function], + default: undefined, + validator: (val: any) => (isFunction(val) ? !val() : !val), + }, +} as const) + +export const useEmptyValues = ( + props: ExtractPropTypes, + defaultValue?: null | undefined +) => { + const config = useGlobalConfig() + config.value = config.value || {} + + const emptyValues = computed( + () => props.emptyValues || config.value.emptyValues || DEFAULT_EMPTY_VALUES + ) + + const valueOnClear = computed(() => { + // function is used for undefined cause undefined can't be a value of prop + if (isFunction(props.valueOnClear)) { + return props.valueOnClear() + } else if (props.valueOnClear !== undefined) { + return props.valueOnClear + } else if (isFunction(config.value.valueOnClear)) { + return config.value.valueOnClear() + } else if (config.value.valueOnClear !== undefined) { + return config.value.valueOnClear + } + return defaultValue !== undefined ? defaultValue : DEFAULT_VALUE_ON_CLEAR + }) + + const isEmptyValue = (value: any) => { + return emptyValues.value.includes(value) + } + + if (!emptyValues.value.includes(valueOnClear.value)) { + debugWarn(SCOPE, 'value-on-clear should be a value of empty-values') + } + + return { + emptyValues, + valueOnClear, + isEmptyValue, + } +}