feat: add default selection option to formkit select component (#6538)

#### What type of PR is this?

/kind improvement
/area ui
/milestone 2.19.x

#### What this PR does / why we need it:

为 formkit select 增加属性 `autoSelect`。

这个属性仅会作用于单选,且目的是为了初始化数据所用。当 value 或者 placeholder 存在时,此属性无效。

会查找第一个 option 不为 disabled 的数据。

#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/6548/head
Takagi 2024-08-29 12:15:24 +08:00 committed by GitHub
parent ad267ebed7
commit 19049c1302
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 87 additions and 37 deletions

View File

@ -71,6 +71,7 @@
10. `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。
11. `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。
12. `searchable`:是否支持搜索内容,默认为 `false`
13. `autoSelect`:当 value 不存在时,是否自动选择第一个选项,默认为 `true`。仅在单选时有效。
在 Vue 单组件中使用:
@ -120,16 +121,16 @@ select 是一个选择器类型的输入组件,使用者可以从一批待选
multiple
searchable
:options="[
{ label: "China", value: "China" },
{ label: "USA", value: "USA" },
{ label: "Japan", value: "Japan" },
{ label: "Korea", value: "Korea" },
{ label: "France", value: "France" },
{ label: "Italy", value: "Italy" },
{ label: "Germany", value: "Germany" },
{ label: "UK", value: "UK" },
{ label: "Canada", value: "Canada" },
{ label: "Australia", value: "Australia" },
{ label: 'China', value: 'China' },
{ label: 'USA', value: 'USA' },
{ label: 'Japan', value: 'Japan' },
{ label: 'Korea', value: 'Korea' },
{ label: 'France', value: 'France' },
{ label: 'Italy', value: 'Italy' },
{ label: 'Germany', value: 'Germany' },
{ label: 'UK', value: 'UK' },
{ label: 'Canada', value: 'Canada' },
{ label: 'Australia', value: 'Australia' },
]"
help="Dont worry, you cant get this one wrong."
/>
@ -196,6 +197,8 @@ const handleSelectPostAuthorRemote = {
clearable: true
placeholder: Select a country
options:
- label: China
value: cn
- label: France
value: fr
- label: Germany
@ -231,7 +234,7 @@ const handleSelectPostAuthorRemote = {
```
> [!NOTE]
> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `labelSelector: ${requestOption.valueField}=in(value1,value2,value3)`。
> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `fieldSelector: ${requestOption.valueField}=(value1,value2,value3)`。
> 其中value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。

View File

@ -22,7 +22,6 @@ function optionsHandler(node: FormKitNode) {
export const attachmentGroupSelect: FormKitTypeDefinition = {
...select,
props: ["placeholder"],
forceTypeProp: "select",
features: [optionsHandler],
};

View File

@ -19,7 +19,6 @@ function optionsHandler(node: FormKitNode) {
export const attachmentPolicySelect: FormKitTypeDefinition = {
...select,
props: ["placeholder"],
forceTypeProp: "select",
features: [optionsHandler],
};

View File

@ -21,7 +21,7 @@ function optionsHandler(node: FormKitNode) {
export const menuItemSelect: FormKitTypeDefinition = {
...select,
props: ["placeholder", "menuItems"],
props: ["menuItems", ...(select.props as string[])],
forceTypeProp: "select",
features: [optionsHandler],
};

View File

@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) {
search,
findOptionsByValues,
},
searchable: true,
};
});
}
export const postSelect: FormKitTypeDefinition = {
...select,
props: ["placeholder"],
forceTypeProp: "select",
features: [optionsHandler],
};

View File

@ -37,7 +37,6 @@ function optionsHandler(node: FormKitNode) {
export const roleSelect: FormKitTypeDefinition = {
...select,
props: ["placeholder"],
forceTypeProp: "nativeSelect",
forceTypeProp: "select",
features: [optionsHandler],
};

View File

@ -78,6 +78,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
autoSelect: {
type: Boolean,
default: true,
},
});
const emit = defineEmits<{
@ -221,6 +225,7 @@ const clearAllSelectedOptions = () => {
if (!hasClearable.value) {
return;
}
selectedOptions.value = [];
clearInputValue();
};

View File

@ -77,6 +77,13 @@ export interface SelectProps {
* Whether to enable search, default is false.
*/
searchable?: boolean;
/**
* Whether to automatically select the first option. default is true.
*
* Only valid when `multiple` is false.
*/
autoSelect?: boolean;
}
export interface SelectResponse {
@ -214,6 +221,7 @@ const initSelectProps = () => {
selectProps.allowCreate = !isFalse(nodeProps.allowCreate);
selectProps.clearable = !isFalse(nodeProps.clearable);
selectProps.searchable = !isFalse(nodeProps.searchable);
selectProps.autoSelect = !isFalse(nodeProps.autoSelect) || true;
if (selectProps.remote) {
if (!nodeProps.remoteOption) {
throw new Error("remoteOption is required when remote is true.");
@ -341,37 +349,42 @@ const fetchSelectedOptions = async (): Promise<
> => {
const node = props.context.node;
const value = node.value;
if (!value) {
return undefined;
}
const selectedValues: string[] = [];
const selectedValues: Array<unknown> = [];
if (Array.isArray(value)) {
selectedValues.push(...value);
} else if (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean"
typeof value === "boolean" ||
value === void 0
) {
selectedValues.push(value.toString());
selectedValues.push(value);
}
const currentOptions = options.value?.filter((option) =>
selectedValues.includes(option.value.toString())
selectedValues.includes(option.value)
);
// Get options that are not yet mapped.
const unmappedSelectValues = selectedValues.filter(
(value) => !currentOptions?.find((option) => option.value === value)
);
const unmappedSelectValues = selectedValues
.filter(
(value) => !currentOptions?.find((option) => option.value === value)
)
.filter(Boolean);
if (unmappedSelectValues.length === 0) {
if (!currentOptions || currentOptions.length === 0) {
return;
}
return currentOptions?.sort((a, b) =>
selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1
);
}
// Map the unresolved options to label and value format.
const mappedSelectOptions = await mapUnresolvedOptions(unmappedSelectValues);
const mappedSelectOptions = await mapUnresolvedOptions(
unmappedSelectValues.map(String)
);
// Merge currentOptions and mappedSelectOptions, then sort them according to selectValues order.
return [...(currentOptions || []), ...mappedSelectOptions].sort((a, b) =>
selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1
@ -508,16 +521,47 @@ onMounted(async () => {
}
});
const getAutoSelectedOption = ():
| {
label: string;
value: string;
}
| undefined => {
if (!options.value || options.value.length === 0) {
return;
}
// Find the first option that is not disabled.
return options.value.find((option) => {
const attrs = option.attrs as Record<string, unknown>;
return isFalse(attrs?.disabled as string | boolean | undefined);
});
};
const stopSelectedWatch = watch(
() => [options.value, props.context.value],
async () => {
if (options.value) {
const selectedOption = await fetchSelectedOptions();
selectOptions.value = selectedOption;
if (selectedOption) {
selectOptions.value = selectedOption;
return;
}
const isAutoSelect =
selectProps.autoSelect &&
!selectProps.multiple &&
!selectProps.placeholder &&
!props.context.node.value;
if (isAutoSelect) {
// Automatically select the first option when the selected value is empty.
const autoSelectedOption = getAutoSelectedOption();
if (autoSelectedOption) {
selectOptions.value = [autoSelectedOption];
handleUpdate(selectOptions.value);
}
}
}
},
{
immediate: true,
}
);
@ -669,6 +713,7 @@ const handleNextPage = async () => {
:remote="isRemote"
:clearable="selectProps.clearable"
:searchable="selectProps.searchable"
:auto-select="selectProps.autoSelect"
@update="handleUpdate"
@search="handleSearch"
@load-more="handleNextPage"

View File

@ -52,6 +52,7 @@ export const select: FormKitTypeDefinition = {
"allowCreate",
"remoteOptimize",
"searchable",
"autoSelect",
],
library: {

View File

@ -1,7 +1,7 @@
import { singlePageLabels } from "@/constants/labels";
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs";
import { consoleApiClient } from "@halo-dev/api-client";
import { select } from "./select";
async function search({ page, size, keyword }) {
const { data } = await consoleApiClient.content.singlePage.listSinglePages({
@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) {
search,
findOptionsByValues,
},
searchable: true,
};
});
}
export const singlePageSelect: FormKitTypeDefinition = {
...select,
props: ["placeholder"],
forceTypeProp: "nativeSelect",
features: [optionsHandler, selects, defaultIcon("select", "select")],
forceTypeProp: "select",
features: [optionsHandler],
};

View File

@ -57,7 +57,6 @@ function optionsHandler(node: FormKitNode) {
export const userSelect: FormKitTypeDefinition = {
...select,
props: ["placeholder"],
forceTypeProp: "select",
features: [optionsHandler],
};