Skip to content

Commit 44d9806

Browse files
authored
feat: Implement optionRender API (#987)
* feat: implement optionRender api * chore: clean code * docs: update md * refactor: modify api * refactor: fix type definition
1 parent eda3dca commit 44d9806

File tree

7 files changed

+83
-16
lines changed

7 files changed

+83
-16
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
React Select
66

7+
<!-- prettier-ignore -->
78
[![NPM version][npm-image]][npm-url]
89
[![npm download][download-image]][download-url]
910
[![build status][github-actions-image]][github-actions-url]
@@ -67,6 +68,7 @@ export default () => (
6768

6869
### Select props
6970

71+
<!-- prettier-ignore -->
7072
| name | description | type | default |
7173
| --- | --- | --- | --- |
7274
| id | html id to set on the component wrapper | String | '' |
@@ -126,6 +128,7 @@ export default () => (
126128
| loading | show loading icon in arrow | boolean | false |
127129
| virtual | Disable virtual scroll | boolean | true |
128130
| direction | direction of dropdown | 'ltr' \| 'rtl' | 'ltr' |
131+
| optionRender | Custom rendering options | (oriOption: FlattenOptionData\<BaseOptionType\> , info: { index: number }) => React.ReactNode | - |
129132

130133
### Methods
131134

docs/demo/option-render.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: optionRender
3+
nav:
4+
title: Demo
5+
path: /demo
6+
---
7+
8+
<code src="../examples/option-render.tsx"></code>

docs/examples/option-render.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-disable no-console */
2+
import Select from 'rc-select';
3+
import '../../assets/index.less';
4+
5+
export default () => {
6+
return (
7+
<Select
8+
options={[
9+
{ label: 'test1', value: '1' },
10+
{ label: 'test2', value: '2' },
11+
]}
12+
optionRender={(option, { index }) => {
13+
return `${option.label} - ${index}`;
14+
}}
15+
/>
16+
);
17+
};

src/OptionList.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import classNames from 'classnames';
2-
import useMemo from 'rc-util/lib/hooks/useMemo';
32
import KeyCode from 'rc-util/lib/KeyCode';
3+
import useMemo from 'rc-util/lib/hooks/useMemo';
44
import omit from 'rc-util/lib/omit';
55
import pickAttrs from 'rc-util/lib/pickAttrs';
66
import type { ListRef } from 'rc-virtual-list';
77
import List from 'rc-virtual-list';
88
import type { ScrollConfig } from 'rc-virtual-list/lib/List';
99
import * as React from 'react';
1010
import { useEffect } from 'react';
11-
import useBaseProps from './hooks/useBaseProps';
12-
import type { FlattenOptionData } from './interface';
1311
import type { BaseOptionType, RawValueType } from './Select';
1412
import SelectContext from './SelectContext';
1513
import TransBtn from './TransBtn';
14+
import useBaseProps from './hooks/useBaseProps';
15+
import type { FlattenOptionData } from './interface';
1616
import { isPlatformMac } from './utils/platformUtil';
1717

1818
// export interface OptionListProps<OptionsType extends object[]> {
@@ -56,6 +56,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
5656
direction,
5757
listHeight,
5858
listItemHeight,
59+
optionRender,
5960
} = React.useContext(SelectContext);
6061

6162
const itemPrefixCls = `${prefixCls}-item`;
@@ -366,7 +367,11 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
366367
}}
367368
style={style}
368369
>
369-
<div className={`${optionPrefixCls}-content`}>{content}</div>
370+
<div className={`${optionPrefixCls}-content`}>
371+
{typeof optionRender === 'function'
372+
? optionRender(item, { index: itemIndex })
373+
: content}
374+
</div>
370375
{React.isValidElement(menuItemSelectedIcon) || selected}
371376
{iconVisible && (
372377
<TransBtn

src/Select.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,16 @@ import type {
4141
RenderNode,
4242
} from './BaseSelect';
4343
import BaseSelect, { isMultiple } from './BaseSelect';
44+
import OptGroup from './OptGroup';
45+
import Option from './Option';
46+
import OptionList from './OptionList';
47+
import SelectContext from './SelectContext';
4448
import useCache from './hooks/useCache';
4549
import useFilterOptions from './hooks/useFilterOptions';
4650
import useId from './hooks/useId';
4751
import useOptions from './hooks/useOptions';
4852
import useRefFunc from './hooks/useRefFunc';
49-
import OptGroup from './OptGroup';
50-
import Option from './Option';
51-
import OptionList from './OptionList';
52-
import SelectContext from './SelectContext';
53+
import type { FlattenOptionData } from './interface';
5354
import { hasValue, isComboNoValue, toArray } from './utils/commonUtil';
5455
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
5556
import warningProps, { warningNullOptions } from './utils/warningPropsUtil';
@@ -138,6 +139,10 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
138139
optionLabelProp?: string;
139140
children?: React.ReactNode;
140141
options?: OptionType[];
142+
optionRender?: (
143+
oriOption: FlattenOptionData<BaseOptionType>,
144+
info: { index: number },
145+
) => React.ReactNode;
141146
defaultActiveFirstOption?: boolean;
142147
virtual?: boolean;
143148
direction?: 'ltr' | 'rtl';
@@ -184,6 +189,7 @@ const Select = React.forwardRef(
184189
optionFilterProp,
185190
optionLabelProp,
186191
options,
192+
optionRender,
187193
children,
188194
defaultActiveFirstOption,
189195
menuItemSelectedIcon,
@@ -605,6 +611,7 @@ const Select = React.forwardRef(
605611
listHeight,
606612
listItemHeight,
607613
childrenAsData,
614+
optionRender,
608615
};
609616
}, [
610617
parsedOptions,
@@ -620,6 +627,7 @@ const Select = React.forwardRef(
620627
listHeight,
621628
listItemHeight,
622629
childrenAsData,
630+
optionRender,
623631
]);
624632

625633
// ========================== Warning ===========================

src/SelectContext.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import * as React from 'react';
22
import type { RawValueType, RenderNode } from './BaseSelect';
3+
import type {
4+
BaseOptionType,
5+
FieldNames,
6+
OnActiveValue,
7+
OnInternalSelect,
8+
SelectProps,
9+
} from './Select';
310
import type { FlattenOptionData } from './interface';
4-
import type { BaseOptionType, FieldNames, OnActiveValue, OnInternalSelect } from './Select';
511

612
// Use any here since we do not get the type during compilation
713
export interface SelectContextProps {
814
options: BaseOptionType[];
15+
optionRender?: SelectProps['optionRender'];
916
flattenOptions: FlattenOptionData<BaseOptionType>[];
1017
onActiveValue: OnActiveValue;
1118
defaultActiveFirstOption?: boolean;
@@ -14,7 +21,7 @@ export interface SelectContextProps {
1421
rawValues: Set<RawValueType>;
1522
fieldNames?: FieldNames;
1623
virtual?: boolean;
17-
direction?: "ltr" | "rtl";
24+
direction?: 'ltr' | 'rtl';
1825
listHeight?: number;
1926
listItemHeight?: number;
2027
childrenAsData?: boolean;

tests/Select.test.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ describe('Select.Basic', () => {
268268
expect(wrapper2.find('.rc-select-clear-icon').length).toBeFalsy();
269269

270270
const wrapper3 = mount(
271-
<Select allowClear={{ clearIcon: <div className='custom-clear-icon'>x</div> }} value="1">
271+
<Select allowClear={{ clearIcon: <div className="custom-clear-icon">x</div> }} value="1">
272272
<Option value="1">1</Option>
273273
<Option value="2">2</Option>
274274
</Select>,
@@ -277,17 +277,16 @@ describe('Select.Basic', () => {
277277
expect(wrapper3.find('.custom-clear-icon').text()).toBe('x');
278278

279279
const wrapper4 = mount(
280-
<Select allowClear={{ clearIcon: <div className='custom-clear-icon'>x</div> }}>
280+
<Select allowClear={{ clearIcon: <div className="custom-clear-icon">x</div> }}>
281281
<Option value="1">1</Option>
282282
<Option value="2">2</Option>
283283
</Select>,
284284
);
285285
expect(wrapper4.find('.custom-clear-icon').length).toBeFalsy();
286286

287-
288287
resetWarned();
289288
const wrapper5 = mount(
290-
<Select allowClear clearIcon={<div className='custom-clear-icon'>x</div>} value="1">
289+
<Select allowClear clearIcon={<div className="custom-clear-icon">x</div>} value="1">
291290
<Option value="1">1</Option>
292291
<Option value="2">2</Option>
293292
</Select>,
@@ -296,7 +295,7 @@ describe('Select.Basic', () => {
296295
expect(wrapper5.find('.custom-clear-icon').text()).toBe('x');
297296

298297
const wrapper6 = mount(
299-
<Select allowClear clearIcon={<div className='custom-clear-icon'>x</div>}>
298+
<Select allowClear clearIcon={<div className="custom-clear-icon">x</div>}>
300299
<Option value="1">1</Option>
301300
<Option value="2">2</Option>
302301
</Select>,
@@ -1361,7 +1360,7 @@ describe('Select.Basic', () => {
13611360
beforeAll(() => {
13621361
domHook = spyElementPrototypes(HTMLElement, {
13631362
getBoundingClientRect: () => ({
1364-
width: 1000
1363+
width: 1000,
13651364
}),
13661365
});
13671366
});
@@ -2095,4 +2094,24 @@ describe('Select.Basic', () => {
20952094
const { container } = testingRender(<Select open direction="rtl" options={options} />);
20962095
expect(container.querySelector('.rc-virtual-list-rtl')).toBeTruthy();
20972096
});
2097+
2098+
it('Should optionRender work', () => {
2099+
const options = [
2100+
{ label: 'test1', value: '1' },
2101+
{ label: 'test2', value: '2' },
2102+
];
2103+
2104+
const { container } = testingRender(
2105+
<Select
2106+
open
2107+
options={options}
2108+
optionRender={(option, {index}) => {
2109+
return `${option.label} - ${index}`;
2110+
}}
2111+
/>,
2112+
);
2113+
expect(container.querySelector('.rc-select-item-option-content').innerHTML).toEqual(
2114+
'test1 - 0',
2115+
);
2116+
});
20982117
});

0 commit comments

Comments
 (0)