Skip to content

Commit 0ec5fba

Browse files
authored
feat: Support fieldNames (#641)
* chore: Init fieldNames * fix: Mapping logic * docs: add demo * test: more test case
1 parent ad2dda0 commit 0ec5fba

File tree

8 files changed

+182
-28
lines changed

8 files changed

+182
-28
lines changed

examples/singleFieldNames.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import Select from '../src';
4+
import '../assets/index.less';
5+
import './single.less';
6+
7+
export default () => {
8+
return (
9+
<Select
10+
style={{ width: 500 }}
11+
onChange={console.log}
12+
fieldNames={{
13+
label: 'fieldLabel',
14+
value: 'fieldValue',
15+
options: 'fieldOptions',
16+
}}
17+
options={
18+
[
19+
{
20+
fieldLabel: 'Group',
21+
fieldOptions: [
22+
{
23+
fieldLabel: 'Bamboo',
24+
fieldValue: 'bamboo',
25+
},
26+
{
27+
fieldLabel: 'Light',
28+
fieldValue: 'light',
29+
},
30+
],
31+
},
32+
] as any
33+
}
34+
/>
35+
);
36+
};
37+
/* eslint-enable */

src/OptionList.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import KeyCode from 'rc-util/lib/KeyCode';
3+
import omit from 'rc-util/lib/omit';
34
import pickAttrs from 'rc-util/lib/pickAttrs';
45
import useMemo from 'rc-util/lib/hooks/useMemo';
56
import classNames from 'classnames';
@@ -12,13 +13,16 @@ import type {
1213
OptionData,
1314
RenderNode,
1415
OnActiveValue,
16+
FieldNames,
1517
} from './interface';
1618
import type { RawValueType, FlattenOptionsType } from './interface/generator';
19+
import { fillFieldNames } from './utils/valueUtil';
1720

1821
export interface OptionListProps<OptionsType extends object[]> {
1922
prefixCls: string;
2023
id: string;
2124
options: OptionsType;
25+
fieldNames?: FieldNames;
2226
flattenOptions: FlattenOptionsType<OptionsType>;
2327
height: number;
2428
itemHeight: number;
@@ -59,6 +63,7 @@ const OptionList: React.RefForwardingComponent<
5963
{
6064
prefixCls,
6165
id,
66+
fieldNames,
6267
flattenOptions,
6368
childrenAsData,
6469
values,
@@ -246,7 +251,9 @@ const OptionList: React.RefForwardingComponent<
246251
);
247252
}
248253

249-
function renderItem(index: number) {
254+
const omitFieldNameList = Object.values(fillFieldNames(fieldNames));
255+
256+
const renderItem = (index: number) => {
250257
const item = memoFlattenOptions[index];
251258
if (!item) return null;
252259

@@ -266,7 +273,7 @@ const OptionList: React.RefForwardingComponent<
266273
{value}
267274
</div>
268275
) : null;
269-
}
276+
};
270277

271278
return (
272279
<>
@@ -287,8 +294,8 @@ const OptionList: React.RefForwardingComponent<
287294
virtual={virtual}
288295
onMouseEnter={onMouseEnter}
289296
>
290-
{({ group, groupOption, data }, itemIndex) => {
291-
const { label, key } = data;
297+
{({ group, groupOption, data, label, value }, itemIndex) => {
298+
const { key } = data;
292299

293300
// Group
294301
if (group) {
@@ -299,15 +306,8 @@ const OptionList: React.RefForwardingComponent<
299306
);
300307
}
301308

302-
const {
303-
disabled,
304-
value,
305-
title,
306-
children,
307-
style,
308-
className,
309-
...otherProps
310-
} = data as OptionData;
309+
const { disabled, title, children, style, className, ...otherProps } = data as OptionData;
310+
const passedProps = omit(otherProps, omitFieldNameList);
311311

312312
// Option
313313
const selected = values.has(value);
@@ -337,7 +337,7 @@ const OptionList: React.RefForwardingComponent<
337337

338338
return (
339339
<div
340-
{...otherProps}
340+
{...passedProps}
341341
aria-selected={selected}
342342
className={optionClassName}
343343
title={optionTitle}

src/generate.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { RefSelectorProps } from './Selector';
1919
import Selector from './Selector';
2020
import type { RefTriggerProps } from './SelectTrigger';
2121
import SelectTrigger from './SelectTrigger';
22-
import type { RenderNode, Mode, RenderDOMFunc, OnActiveValue } from './interface';
22+
import type { RenderNode, Mode, RenderDOMFunc, OnActiveValue, FieldNames } from './interface';
2323
import type {
2424
GetLabeledValue,
2525
FilterOptions,
@@ -84,6 +84,9 @@ export interface SelectProps<OptionsType extends object[], ValueType> extends Re
8484
/** Config max length of input. This is only work when `mode` is `combobox` */
8585
maxLength?: number;
8686

87+
// Field
88+
fieldNames?: FieldNames;
89+
8790
// Search
8891
inputValue?: string;
8992
searchValue?: string;
@@ -276,6 +279,8 @@ export default function generateSelector<
276279
autoClearSearchValue = true,
277280
onSearch,
278281

282+
fieldNames,
283+
279284
// Icons
280285
allowClear,
281286
clearIcon,
@@ -961,6 +966,7 @@ export default function generateSelector<
961966
open={mergedOpen}
962967
childrenAsData={!options}
963968
options={displayOptions}
969+
fieldNames={fieldNames}
964970
flattenOptions={displayFlattenOptions}
965971
multiple={isMultiple}
966972
values={rawValues}

src/hooks/useCacheOptions.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@ export default function useCacheOptions<
1616
const optionMap = React.useMemo(() => {
1717
const map: Map<RawValueType, FlattenOptionsType<OptionsType>[number]> = new Map();
1818
options.forEach((item) => {
19-
const {
20-
data: { value },
21-
} = item;
19+
const { value } = item;
2220
map.set(value, item);
2321
});
2422
return map;
2523
}, [options]);
2624

2725
prevOptionMapRef.current = optionMap;
2826

29-
const getValueOption = (vals: RawValueType[]): FlattenOptionsType<OptionsType> =>
30-
vals.map((value) => prevOptionMapRef.current.get(value)).filter(Boolean);
27+
const getValueOption = (valueList: RawValueType[]): FlattenOptionsType<OptionsType> =>
28+
valueList.map((value) => prevOptionMapRef.current.get(value)).filter(Boolean);
3129

3230
return getValueOption;
3331
}

src/interface/generator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export declare function RefSelectFunc<OptionsType extends object[], ValueType>(
6565
export type FlattenOptionsType<OptionsType extends object[] = object[]> = {
6666
key: Key;
6767
data: OptionsType[number];
68+
label?: React.ReactNode;
69+
value?: RawValueType;
6870
/** Used for customize data */
6971
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
7072
}[];

src/interface/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export type RenderNode = React.ReactNode | ((props: any) => React.ReactNode);
88
export type Mode = 'multiple' | 'tags' | 'combobox';
99

1010
// ======================== Option ========================
11+
export interface FieldNames {
12+
value?: string;
13+
label?: string;
14+
options?: string;
15+
}
16+
1117
export type OnActiveValue = (
1218
active: RawValueType,
1319
index: number,
@@ -49,4 +55,6 @@ export interface FlattenOptionData {
4955
groupOption?: boolean;
5056
key: string | number;
5157
data: OptionData | OptionGroupData;
58+
label?: React.ReactNode;
59+
value?: React.Key;
5260
}

src/utils/valueUtil.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
OptionData,
55
OptionGroupData,
66
FlattenOptionData,
7+
FieldNames,
78
} from '../interface';
89
import type {
910
LabelValueType,
@@ -32,32 +33,56 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
3233
return `rc-index-key-${index}`;
3334
}
3435

36+
export function fillFieldNames(fieldNames?: FieldNames) {
37+
const { label, value, options } = fieldNames || {};
38+
39+
return {
40+
label: label || 'label',
41+
value: value || 'value',
42+
options: options || 'options',
43+
};
44+
}
45+
3546
/**
3647
* Flat options into flatten list.
3748
* We use `optionOnly` here is aim to avoid user use nested option group.
3849
* Here is simply set `key` to the index if not provided.
3950
*/
40-
export function flattenOptions(options: SelectOptionsType): FlattenOptionData[] {
51+
export function flattenOptions(
52+
options: SelectOptionsType,
53+
{ fieldNames }: { fieldNames?: FieldNames } = {},
54+
): FlattenOptionData[] {
4155
const flattenList: FlattenOptionData[] = [];
4256

57+
const {
58+
label: fieldLabel,
59+
value: fieldValue,
60+
options: fieldOptions,
61+
} = fillFieldNames(fieldNames);
62+
4363
function dig(list: SelectOptionsType, isGroupOption: boolean) {
4464
list.forEach((data) => {
45-
if (isGroupOption || !('options' in data)) {
65+
const label = data[fieldLabel];
66+
67+
if (isGroupOption || !(fieldOptions in data)) {
4668
// Option
4769
flattenList.push({
4870
key: getKey(data, flattenList.length),
4971
groupOption: isGroupOption,
5072
data,
73+
label,
74+
value: data[fieldValue],
5175
});
5276
} else {
5377
// Option Group
5478
flattenList.push({
5579
key: getKey(data, flattenList.length),
5680
group: true,
5781
data,
82+
label,
5883
});
5984

60-
dig(data.options, true);
85+
dig(data[fieldOptions], true);
6186
}
6287
});
6388
}
@@ -94,11 +119,10 @@ export function findValueOption(
94119
): OptionData[] {
95120
const optionMap: Map<RawValueType, OptionData> = new Map();
96121

97-
options.forEach((flattenItem) => {
98-
if (!flattenItem.group) {
99-
const data = flattenItem.data as OptionData;
122+
options.forEach(({ data, group, value }) => {
123+
if (!group) {
100124
// Check if match
101-
optionMap.set(data.value, data);
125+
optionMap.set(value, data as OptionData);
102126
}
103127
});
104128

@@ -120,7 +144,7 @@ export function findValueOption(
120144
export const getLabeledValue: GetLabeledValue<FlattenOptionData[]> = (
121145
value,
122146
{ options, prevValueMap, labelInValue, optionLabelProp },
123-
) => {
147+
): LabelValueType => {
124148
const item = findValueOption([value], options)[0];
125149
const result: LabelValueType = {
126150
value,

tests/Field.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/* eslint-disable import/no-named-as-default-member */
2+
import { mount } from 'enzyme';
3+
import { act } from 'react-dom/test-utils';
4+
import * as React from 'react';
5+
import Select from '../src';
6+
import type { SelectProps } from '../src';
7+
import { injectRunAllTimers } from './utils/common';
8+
9+
describe('Select.Field', () => {
10+
injectRunAllTimers(jest);
11+
12+
beforeEach(() => {
13+
jest.useFakeTimers();
14+
});
15+
16+
afterEach(() => {
17+
jest.useRealTimers();
18+
});
19+
20+
const OPTION_1 = { bambooLabel: 'Light', bambooValue: 'light' };
21+
const OPTION_2 = { bambooLabel: 'Little', bambooValue: 'little' };
22+
23+
function mountSelect(props?: Partial<SelectProps<any>>) {
24+
return mount(
25+
<Select
26+
open
27+
options={
28+
[
29+
{
30+
bambooLabel: 'Bamboo',
31+
bambooChildren: [OPTION_1, OPTION_2],
32+
},
33+
] as any
34+
}
35+
fieldNames={{
36+
label: 'bambooLabel',
37+
value: 'bambooValue',
38+
options: 'bambooChildren',
39+
}}
40+
{...props}
41+
/>,
42+
);
43+
}
44+
45+
it('fieldNames should work', () => {
46+
const onChange = jest.fn();
47+
const onSelect = jest.fn();
48+
49+
const wrapper = mountSelect({ onChange, onSelect });
50+
51+
act(() => {
52+
jest.runAllTimers();
53+
});
54+
55+
// Label match
56+
expect(wrapper.find('.rc-select-item-group').text()).toEqual('Bamboo');
57+
expect(wrapper.find('.rc-select-item-option').first().text()).toEqual('Light');
58+
expect(wrapper.find('.rc-select-item-option').last().text()).toEqual('Little');
59+
60+
// Click
61+
wrapper.find('.rc-select-item-option-content').last().simulate('click');
62+
expect(onChange).toHaveBeenCalledWith('little', OPTION_2);
63+
expect(onSelect).toHaveBeenCalledWith('little', OPTION_2);
64+
});
65+
66+
it('multiple', () => {
67+
const onChange = jest.fn();
68+
const wrapper = mountSelect({ mode: 'multiple', onChange });
69+
70+
// First one
71+
wrapper.find('.rc-select-item-option-content').first().simulate('click');
72+
expect(onChange).toHaveBeenCalledWith(['light'], [OPTION_1]);
73+
74+
// Last one
75+
onChange.mockReset();
76+
wrapper.find('.rc-select-item-option-content').last().simulate('click');
77+
expect(onChange).toHaveBeenCalledWith(['light', 'little'], [OPTION_1, OPTION_2]);
78+
});
79+
});

0 commit comments

Comments
 (0)