@etsoo/materialui 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/SelectEx.tsx +30 -3
- package/lib/HiSelector.d.ts +62 -0
- package/lib/HiSelector.js +55 -0
- package/lib/SelectEx.d.ts +4 -0
- package/lib/SelectEx.js +28 -14
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +12 -12
- package/src/HiSelector.tsx +221 -0
- package/src/SelectEx.tsx +36 -8
- package/src/index.ts +1 -0
package/__tests__/SelectEx.tsx
CHANGED
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
|
2
2
|
import { SelectEx } from '../src';
|
|
3
3
|
import { findByText, fireEvent, render, screen } from '@testing-library/react';
|
|
4
4
|
import '@testing-library/jest-dom/extend-expect';
|
|
5
|
+
import { Utils } from '@etsoo/shared';
|
|
6
|
+
import { act } from 'react-dom/test-utils';
|
|
5
7
|
|
|
6
8
|
it('Render SelectEx', async () => {
|
|
7
9
|
// Arrange
|
|
@@ -11,16 +13,41 @@ it('Render SelectEx', async () => {
|
|
|
11
13
|
{ id: 2, name: 'Name 2' }
|
|
12
14
|
];
|
|
13
15
|
|
|
16
|
+
Utils.addBlankItem(options, 'id', 'name');
|
|
17
|
+
|
|
18
|
+
const itemChangeCallback = jest.fn((option, userAction) => {
|
|
19
|
+
if (userAction) expect(option).toBeUndefined();
|
|
20
|
+
else expect(option.id).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
14
23
|
// Render component
|
|
15
24
|
const { baseElement } = render(
|
|
16
|
-
<SelectEx<T>
|
|
25
|
+
<SelectEx<T>
|
|
26
|
+
options={options}
|
|
27
|
+
name="test"
|
|
28
|
+
onItemChange={itemChangeCallback}
|
|
29
|
+
value={1}
|
|
30
|
+
search
|
|
31
|
+
labelField="name"
|
|
32
|
+
/>
|
|
17
33
|
);
|
|
18
34
|
|
|
35
|
+
expect(itemChangeCallback).toBeCalled();
|
|
36
|
+
|
|
19
37
|
// Act, click to show the list
|
|
20
38
|
const button = screen.getByRole('button');
|
|
21
39
|
fireEvent.mouseDown(button); // Not click
|
|
22
40
|
|
|
23
41
|
// Get list item
|
|
24
|
-
const
|
|
25
|
-
expect(
|
|
42
|
+
const itemName2 = await findByText(baseElement, 'Name 2');
|
|
43
|
+
expect(itemName2.nodeName).toBe('SPAN');
|
|
44
|
+
|
|
45
|
+
const itemBlank = await findByText(baseElement, '---');
|
|
46
|
+
expect(itemBlank.nodeName).toBe('SPAN');
|
|
47
|
+
|
|
48
|
+
act(() => {
|
|
49
|
+
itemBlank.click();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(itemChangeCallback).toBeCalledTimes(2);
|
|
26
53
|
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { DataTypes, IdDefaultType, LabelDefaultType } from '@etsoo/shared';
|
|
2
|
+
import { SelectChangeEvent } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Hierarchy selector props
|
|
6
|
+
*/
|
|
7
|
+
export declare type HiSelectorProps<T extends object, D extends DataTypes.Keys<T> = IdDefaultType<T>, L extends DataTypes.Keys<T, string> = LabelDefaultType<T>> = {
|
|
8
|
+
/**
|
|
9
|
+
* Id field
|
|
10
|
+
*/
|
|
11
|
+
idField?: D;
|
|
12
|
+
/**
|
|
13
|
+
* Error
|
|
14
|
+
*/
|
|
15
|
+
error?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The helper text content.
|
|
18
|
+
*/
|
|
19
|
+
helperText?: React.ReactNode;
|
|
20
|
+
/**
|
|
21
|
+
* Name, also hidden input field name
|
|
22
|
+
*/
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* Label
|
|
26
|
+
*/
|
|
27
|
+
label?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Label field
|
|
30
|
+
*/
|
|
31
|
+
labelField?: L;
|
|
32
|
+
/**
|
|
33
|
+
* Load data callback
|
|
34
|
+
*/
|
|
35
|
+
loadData: (parent?: T[D]) => PromiseLike<T[] | null | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* On value change event
|
|
38
|
+
*/
|
|
39
|
+
onChange?: (value: unknown) => void;
|
|
40
|
+
/**
|
|
41
|
+
* On select change event
|
|
42
|
+
*/
|
|
43
|
+
onSelectChange?: (e: SelectChangeEvent<unknown>) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Item change callback
|
|
46
|
+
*/
|
|
47
|
+
onItemChange?: (option: T | undefined, userAction: boolean) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Required
|
|
50
|
+
*/
|
|
51
|
+
required?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Values
|
|
54
|
+
*/
|
|
55
|
+
values?: T[D][];
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Hierarchy selector
|
|
59
|
+
* @param props Prop
|
|
60
|
+
* @returns Component
|
|
61
|
+
*/
|
|
62
|
+
export declare function HiSelector<T extends object, D extends DataTypes.Keys<T> = IdDefaultType<T>, L extends DataTypes.Keys<T, string> = LabelDefaultType<T>>(props: HiSelectorProps<T, D, L>): JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { FormLabel, Grid } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { SelectEx } from './SelectEx';
|
|
4
|
+
/**
|
|
5
|
+
* Hierarchy selector
|
|
6
|
+
* @param props Prop
|
|
7
|
+
* @returns Component
|
|
8
|
+
*/
|
|
9
|
+
export function HiSelector(props) {
|
|
10
|
+
// Destruct
|
|
11
|
+
const { idField = 'id', error, helperText, name, label = name, labelField = 'name', loadData, onChange, onSelectChange, onItemChange, required, values = [] } = props;
|
|
12
|
+
const [localValues, setValues] = React.useState(values);
|
|
13
|
+
const updateValue = (value) => {
|
|
14
|
+
if (onChange)
|
|
15
|
+
onChange(value);
|
|
16
|
+
};
|
|
17
|
+
const doChange = (event, index) => {
|
|
18
|
+
const value = event.target.value;
|
|
19
|
+
const itemValue = value === '' ? undefined : value;
|
|
20
|
+
updateValue(itemValue);
|
|
21
|
+
const newValues = [...localValues.slice(0, index)];
|
|
22
|
+
if (itemValue != null)
|
|
23
|
+
newValues.push(itemValue);
|
|
24
|
+
setValues(newValues);
|
|
25
|
+
if (onSelectChange)
|
|
26
|
+
onSelectChange(event);
|
|
27
|
+
};
|
|
28
|
+
const doItemChange = (option, userAction) => {
|
|
29
|
+
if (onItemChange == null)
|
|
30
|
+
return;
|
|
31
|
+
if (!userAction &&
|
|
32
|
+
(option == null || option[idField] !== values.at(-1)))
|
|
33
|
+
return;
|
|
34
|
+
onItemChange(option, userAction);
|
|
35
|
+
};
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (values.length > 0) {
|
|
38
|
+
setValues(values);
|
|
39
|
+
updateValue(values.at(-1));
|
|
40
|
+
}
|
|
41
|
+
}, [values]);
|
|
42
|
+
const currentValue = localValues.at(-1);
|
|
43
|
+
return (React.createElement(React.Fragment, null,
|
|
44
|
+
React.createElement(Grid, { item: true, xs: 12 },
|
|
45
|
+
React.createElement(FormLabel, { required: required, sx: { fontSize: (theme) => theme.typography.caption } }, label),
|
|
46
|
+
React.createElement("input", { type: "hidden", name: name, value: `${currentValue !== null && currentValue !== void 0 ? currentValue : ''}` })),
|
|
47
|
+
React.createElement(Grid, { item: true, xs: 6, md: 4, lg: 3 },
|
|
48
|
+
React.createElement(SelectEx, { idField: idField, labelField: labelField, name: "tab1", search: true, fullWidth: true, loadData: () => loadData(), value: values[0], onChange: (event) => doChange(event, 0), onItemChange: doItemChange, inputRequired: required, error: error, helperText: helperText })),
|
|
49
|
+
localValues[0] != null && (React.createElement(Grid, { item: true, xs: 6, md: 4, lg: 3 },
|
|
50
|
+
React.createElement(SelectEx, { key: `${localValues[0]}`, idField: idField, labelField: labelField, name: "tab2", search: true, fullWidth: true, loadData: () => loadData(localValues[0]), value: values[1], onChange: (event) => doChange(event, 1), onItemChange: doItemChange }))),
|
|
51
|
+
localValues[1] != null && (React.createElement(Grid, { item: true, xs: 6, md: 4, lg: 3 },
|
|
52
|
+
React.createElement(SelectEx, { key: `${localValues[1]}`, idField: idField, labelField: labelField, name: "tab3", search: true, fullWidth: true, loadData: () => loadData(localValues[1]), value: values[2], onChange: (event) => doChange(event, 2), onItemChange: doItemChange }))),
|
|
53
|
+
localValues[2] != null && (React.createElement(Grid, { item: true, xs: 6, md: 4, lg: 3 },
|
|
54
|
+
React.createElement(SelectEx, { key: `${localValues[2]}`, idField: idField, labelField: labelField, name: "tab4", search: true, fullWidth: true, loadData: () => loadData(localValues[2]), value: values[3], onChange: (event) => doChange(event, 3), onItemChange: doItemChange })))));
|
|
55
|
+
}
|
package/lib/SelectEx.d.ts
CHANGED
|
@@ -37,6 +37,10 @@ export declare type SelectExProps<T extends object, D extends DataTypes.Keys<T>
|
|
|
37
37
|
* Load data callback
|
|
38
38
|
*/
|
|
39
39
|
loadData?: () => PromiseLike<T[] | null | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Item change callback
|
|
42
|
+
*/
|
|
43
|
+
onItemChange?: (option: T | undefined, userAction: boolean) => void;
|
|
40
44
|
/**
|
|
41
45
|
* Item click handler
|
|
42
46
|
*/
|
package/lib/SelectEx.js
CHANGED
|
@@ -3,7 +3,6 @@ import React from 'react';
|
|
|
3
3
|
import { MUGlobal } from './MUGlobal';
|
|
4
4
|
import { ListItemRightIcon } from './ListItemRightIcon';
|
|
5
5
|
import { Utils } from '@etsoo/shared';
|
|
6
|
-
import { ReactUtils } from '@etsoo/react';
|
|
7
6
|
/**
|
|
8
7
|
* Extended select component
|
|
9
8
|
* @param props Props
|
|
@@ -12,17 +11,33 @@ import { ReactUtils } from '@etsoo/react';
|
|
|
12
11
|
export function SelectEx(props) {
|
|
13
12
|
var _a;
|
|
14
13
|
// Destruct
|
|
15
|
-
const { defaultValue, idField = 'id', error, helperText, inputRequired, itemIconRenderer, itemStyle, label, labelField = 'label', loadData, onItemClick, onLoadData, multiple = false, name, options
|
|
14
|
+
const { defaultValue, idField = 'id', error, helperText, inputRequired, itemIconRenderer, itemStyle, label, labelField = 'label', loadData, onItemChange, onItemClick, onLoadData, multiple = false, name, options, search = false, autoAddBlankItem = search, value, onChange, fullWidth, ...rest } = props;
|
|
16
15
|
// Options state
|
|
17
|
-
const [localOptions, setOptions] = React.useState(
|
|
16
|
+
const [localOptions, setOptions] = React.useState([]);
|
|
18
17
|
const isMounted = React.useRef(true);
|
|
18
|
+
const doItemChange = (options, value, userAction) => {
|
|
19
|
+
if (onItemChange == null)
|
|
20
|
+
return;
|
|
21
|
+
if (value == null || value === '') {
|
|
22
|
+
onItemChange(undefined, userAction);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const option = options.find((option) => option[idField] === value);
|
|
26
|
+
onItemChange(option, userAction);
|
|
27
|
+
};
|
|
28
|
+
const setOptionsAdd = (options) => {
|
|
29
|
+
setOptions(options);
|
|
30
|
+
if (localValue != null && localValue !== '')
|
|
31
|
+
doItemChange(options, localValue, false);
|
|
32
|
+
};
|
|
19
33
|
// When options change
|
|
20
34
|
// [options] will cause infinite loop
|
|
21
35
|
const propertyWay = loadData == null;
|
|
22
36
|
React.useEffect(() => {
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
if (options == null || !propertyWay)
|
|
38
|
+
return;
|
|
39
|
+
setOptionsAdd(options);
|
|
40
|
+
}, [options, propertyWay]);
|
|
26
41
|
// Local value
|
|
27
42
|
const valueSource = (_a = defaultValue !== null && defaultValue !== void 0 ? defaultValue : value) !== null && _a !== void 0 ? _a : '';
|
|
28
43
|
let localValue;
|
|
@@ -59,14 +74,15 @@ export function SelectEx(props) {
|
|
|
59
74
|
};
|
|
60
75
|
// Set item
|
|
61
76
|
const setItemValue = (id) => {
|
|
62
|
-
var _a;
|
|
63
77
|
if (id != valueState) {
|
|
64
78
|
setValueState(id);
|
|
65
|
-
|
|
79
|
+
/*
|
|
80
|
+
const input = divRef.current?.querySelector('input');
|
|
66
81
|
if (input) {
|
|
67
82
|
// Different value, trigger change event
|
|
68
|
-
ReactUtils.triggerChange(input, id, false);
|
|
83
|
+
ReactUtils.triggerChange(input, id as string, false);
|
|
69
84
|
}
|
|
85
|
+
*/
|
|
70
86
|
}
|
|
71
87
|
};
|
|
72
88
|
// Get option id
|
|
@@ -92,7 +108,7 @@ export function SelectEx(props) {
|
|
|
92
108
|
if (autoAddBlankItem) {
|
|
93
109
|
Utils.addBlankItem(result, idField, labelField);
|
|
94
110
|
}
|
|
95
|
-
|
|
111
|
+
setOptionsAdd(result);
|
|
96
112
|
});
|
|
97
113
|
}
|
|
98
114
|
}, [localValue]);
|
|
@@ -121,8 +137,8 @@ export function SelectEx(props) {
|
|
|
121
137
|
: '', input: React.createElement(OutlinedInput, { notched: true, label: label, required: inputRequired }), labelId: labelId, name: name, multiple: multiple, onChange: (event, child) => {
|
|
122
138
|
if (onChange)
|
|
123
139
|
onChange(event, child);
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
doItemChange(localOptions, event.target.value, true);
|
|
141
|
+
handleChange(event);
|
|
126
142
|
}, renderValue: (selected) => {
|
|
127
143
|
// The text shows up
|
|
128
144
|
return localOptions
|
|
@@ -146,8 +162,6 @@ export function SelectEx(props) {
|
|
|
146
162
|
if (event.defaultPrevented)
|
|
147
163
|
return;
|
|
148
164
|
}
|
|
149
|
-
if (!multiple)
|
|
150
|
-
setItemValue(id);
|
|
151
165
|
}, style: itemStyle == null
|
|
152
166
|
? undefined
|
|
153
167
|
: itemStyle(option) },
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@emotion/css": "^11.10.0",
|
|
52
52
|
"@emotion/react": "^11.10.4",
|
|
53
53
|
"@emotion/styled": "^11.10.4",
|
|
54
|
-
"@etsoo/appscript": "^1.2.
|
|
54
|
+
"@etsoo/appscript": "^1.2.89",
|
|
55
55
|
"@etsoo/notificationbase": "^1.1.7",
|
|
56
|
-
"@etsoo/react": "^1.5.
|
|
57
|
-
"@etsoo/shared": "^1.1.
|
|
56
|
+
"@etsoo/react": "^1.5.87",
|
|
57
|
+
"@etsoo/shared": "^1.1.52",
|
|
58
58
|
"@mui/icons-material": "^5.10.3",
|
|
59
|
-
"@mui/material": "^5.10.
|
|
59
|
+
"@mui/material": "^5.10.4",
|
|
60
60
|
"@types/pica": "^9.0.1",
|
|
61
61
|
"@types/pulltorefreshjs": "^0.1.5",
|
|
62
62
|
"@types/react": "^18.0.18",
|
|
@@ -76,19 +76,19 @@
|
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@babel/cli": "^7.18.10",
|
|
79
|
-
"@babel/core": "^7.
|
|
79
|
+
"@babel/core": "^7.19.0",
|
|
80
80
|
"@babel/plugin-transform-runtime": "^7.18.10",
|
|
81
|
-
"@babel/preset-env": "^7.
|
|
82
|
-
"@babel/runtime-corejs3": "^7.
|
|
81
|
+
"@babel/preset-env": "^7.19.0",
|
|
82
|
+
"@babel/runtime-corejs3": "^7.19.0",
|
|
83
83
|
"@testing-library/jest-dom": "^5.16.5",
|
|
84
|
-
"@testing-library/react": "^13.
|
|
84
|
+
"@testing-library/react": "^13.4.0",
|
|
85
85
|
"@types/jest": "^29.0.0",
|
|
86
|
-
"@typescript-eslint/eslint-plugin": "^5.36.
|
|
87
|
-
"@typescript-eslint/parser": "^5.36.
|
|
86
|
+
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
|
87
|
+
"@typescript-eslint/parser": "^5.36.2",
|
|
88
88
|
"eslint": "^8.23.0",
|
|
89
89
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
90
90
|
"eslint-plugin-import": "^2.26.0",
|
|
91
|
-
"eslint-plugin-react": "^7.31.
|
|
91
|
+
"eslint-plugin-react": "^7.31.7",
|
|
92
92
|
"jest": "^28.1.3",
|
|
93
93
|
"jest-environment-jsdom": "^28.1.3",
|
|
94
94
|
"ts-jest": "^28.0.8",
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { DataTypes, IdDefaultType, LabelDefaultType } from '@etsoo/shared';
|
|
2
|
+
import { FormLabel, Grid, SelectChangeEvent } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { SelectEx } from './SelectEx';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hierarchy selector props
|
|
8
|
+
*/
|
|
9
|
+
export type HiSelectorProps<
|
|
10
|
+
T extends object,
|
|
11
|
+
D extends DataTypes.Keys<T> = IdDefaultType<T>,
|
|
12
|
+
L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
|
|
13
|
+
> = {
|
|
14
|
+
/**
|
|
15
|
+
* Id field
|
|
16
|
+
*/
|
|
17
|
+
idField?: D;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Error
|
|
21
|
+
*/
|
|
22
|
+
error?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The helper text content.
|
|
26
|
+
*/
|
|
27
|
+
helperText?: React.ReactNode;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Name, also hidden input field name
|
|
31
|
+
*/
|
|
32
|
+
name: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Label
|
|
36
|
+
*/
|
|
37
|
+
label?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Label field
|
|
41
|
+
*/
|
|
42
|
+
labelField?: L;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load data callback
|
|
46
|
+
*/
|
|
47
|
+
loadData: (parent?: T[D]) => PromiseLike<T[] | null | undefined>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* On value change event
|
|
51
|
+
*/
|
|
52
|
+
onChange?: (value: unknown) => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* On select change event
|
|
56
|
+
*/
|
|
57
|
+
onSelectChange?: (e: SelectChangeEvent<unknown>) => void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Item change callback
|
|
61
|
+
*/
|
|
62
|
+
onItemChange?: (option: T | undefined, userAction: boolean) => void;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Required
|
|
66
|
+
*/
|
|
67
|
+
required?: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Values
|
|
71
|
+
*/
|
|
72
|
+
values?: T[D][];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Hierarchy selector
|
|
77
|
+
* @param props Prop
|
|
78
|
+
* @returns Component
|
|
79
|
+
*/
|
|
80
|
+
export function HiSelector<
|
|
81
|
+
T extends object,
|
|
82
|
+
D extends DataTypes.Keys<T> = IdDefaultType<T>,
|
|
83
|
+
L extends DataTypes.Keys<T, string> = LabelDefaultType<T>
|
|
84
|
+
>(props: HiSelectorProps<T, D, L>) {
|
|
85
|
+
// Destruct
|
|
86
|
+
const {
|
|
87
|
+
idField = 'id' as D,
|
|
88
|
+
error,
|
|
89
|
+
helperText,
|
|
90
|
+
name,
|
|
91
|
+
label = name,
|
|
92
|
+
labelField = 'name' as L,
|
|
93
|
+
loadData,
|
|
94
|
+
onChange,
|
|
95
|
+
onSelectChange,
|
|
96
|
+
onItemChange,
|
|
97
|
+
required,
|
|
98
|
+
values = []
|
|
99
|
+
} = props;
|
|
100
|
+
|
|
101
|
+
// Value type
|
|
102
|
+
type ValueType = T[D];
|
|
103
|
+
const [localValues, setValues] = React.useState<ValueType[]>(values);
|
|
104
|
+
|
|
105
|
+
const updateValue = (value?: T[D]) => {
|
|
106
|
+
if (onChange) onChange(value);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const doChange = (event: SelectChangeEvent<unknown>, index: number) => {
|
|
110
|
+
const value = event.target.value;
|
|
111
|
+
const itemValue = value === '' ? undefined : (value as T[D]);
|
|
112
|
+
updateValue(itemValue);
|
|
113
|
+
|
|
114
|
+
const newValues = [...localValues.slice(0, index)];
|
|
115
|
+
if (itemValue != null) newValues.push(itemValue);
|
|
116
|
+
setValues(newValues);
|
|
117
|
+
|
|
118
|
+
if (onSelectChange) onSelectChange(event);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const doItemChange = (option: T | undefined, userAction: boolean) => {
|
|
122
|
+
if (onItemChange == null) return;
|
|
123
|
+
if (
|
|
124
|
+
!userAction &&
|
|
125
|
+
(option == null || option[idField] !== values.at(-1))
|
|
126
|
+
)
|
|
127
|
+
return;
|
|
128
|
+
onItemChange(option, userAction);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
React.useEffect(() => {
|
|
132
|
+
if (values.length > 0) {
|
|
133
|
+
setValues(values);
|
|
134
|
+
updateValue(values.at(-1));
|
|
135
|
+
}
|
|
136
|
+
}, [values]);
|
|
137
|
+
|
|
138
|
+
const currentValue = localValues.at(-1);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<React.Fragment>
|
|
142
|
+
<Grid item xs={12}>
|
|
143
|
+
<FormLabel
|
|
144
|
+
required={required}
|
|
145
|
+
sx={{ fontSize: (theme) => theme.typography.caption }}
|
|
146
|
+
>
|
|
147
|
+
{label}
|
|
148
|
+
</FormLabel>
|
|
149
|
+
<input
|
|
150
|
+
type="hidden"
|
|
151
|
+
name={name}
|
|
152
|
+
value={`${currentValue ?? ''}`}
|
|
153
|
+
/>
|
|
154
|
+
</Grid>
|
|
155
|
+
<Grid item xs={6} md={4} lg={3}>
|
|
156
|
+
<SelectEx<T, D, L>
|
|
157
|
+
idField={idField}
|
|
158
|
+
labelField={labelField}
|
|
159
|
+
name="tab1"
|
|
160
|
+
search
|
|
161
|
+
fullWidth
|
|
162
|
+
loadData={() => loadData()}
|
|
163
|
+
value={values[0]}
|
|
164
|
+
onChange={(event) => doChange(event, 0)}
|
|
165
|
+
onItemChange={doItemChange}
|
|
166
|
+
inputRequired={required}
|
|
167
|
+
error={error}
|
|
168
|
+
helperText={helperText}
|
|
169
|
+
/>
|
|
170
|
+
</Grid>
|
|
171
|
+
{localValues[0] != null && (
|
|
172
|
+
<Grid item xs={6} md={4} lg={3}>
|
|
173
|
+
<SelectEx<T, D, L>
|
|
174
|
+
key={`${localValues[0]}`}
|
|
175
|
+
idField={idField}
|
|
176
|
+
labelField={labelField}
|
|
177
|
+
name="tab2"
|
|
178
|
+
search
|
|
179
|
+
fullWidth
|
|
180
|
+
loadData={() => loadData(localValues[0])}
|
|
181
|
+
value={values[1]}
|
|
182
|
+
onChange={(event) => doChange(event, 1)}
|
|
183
|
+
onItemChange={doItemChange}
|
|
184
|
+
/>
|
|
185
|
+
</Grid>
|
|
186
|
+
)}
|
|
187
|
+
{localValues[1] != null && (
|
|
188
|
+
<Grid item xs={6} md={4} lg={3}>
|
|
189
|
+
<SelectEx<T, D, L>
|
|
190
|
+
key={`${localValues[1]}`}
|
|
191
|
+
idField={idField}
|
|
192
|
+
labelField={labelField}
|
|
193
|
+
name="tab3"
|
|
194
|
+
search
|
|
195
|
+
fullWidth
|
|
196
|
+
loadData={() => loadData(localValues[1])}
|
|
197
|
+
value={values[2]}
|
|
198
|
+
onChange={(event) => doChange(event, 2)}
|
|
199
|
+
onItemChange={doItemChange}
|
|
200
|
+
/>
|
|
201
|
+
</Grid>
|
|
202
|
+
)}
|
|
203
|
+
{localValues[2] != null && (
|
|
204
|
+
<Grid item xs={6} md={4} lg={3}>
|
|
205
|
+
<SelectEx<T, D, L>
|
|
206
|
+
key={`${localValues[2]}`}
|
|
207
|
+
idField={idField}
|
|
208
|
+
labelField={labelField}
|
|
209
|
+
name="tab4"
|
|
210
|
+
search
|
|
211
|
+
fullWidth
|
|
212
|
+
loadData={() => loadData(localValues[2])}
|
|
213
|
+
value={values[3]}
|
|
214
|
+
onChange={(event) => doChange(event, 3)}
|
|
215
|
+
onItemChange={doItemChange}
|
|
216
|
+
/>
|
|
217
|
+
</Grid>
|
|
218
|
+
)}
|
|
219
|
+
</React.Fragment>
|
|
220
|
+
);
|
|
221
|
+
}
|
package/src/SelectEx.tsx
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
ListType,
|
|
21
21
|
Utils
|
|
22
22
|
} from '@etsoo/shared';
|
|
23
|
-
import { ReactUtils } from '@etsoo/react';
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
25
|
* Extended select component props
|
|
@@ -70,6 +69,11 @@ export type SelectExProps<
|
|
|
70
69
|
*/
|
|
71
70
|
loadData?: () => PromiseLike<T[] | null | undefined>;
|
|
72
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Item change callback
|
|
74
|
+
*/
|
|
75
|
+
onItemChange?: (option: T | undefined, userAction: boolean) => void;
|
|
76
|
+
|
|
73
77
|
/**
|
|
74
78
|
* Item click handler
|
|
75
79
|
*/
|
|
@@ -113,11 +117,12 @@ export function SelectEx<
|
|
|
113
117
|
label,
|
|
114
118
|
labelField = 'label' as L,
|
|
115
119
|
loadData,
|
|
120
|
+
onItemChange,
|
|
116
121
|
onItemClick,
|
|
117
122
|
onLoadData,
|
|
118
123
|
multiple = false,
|
|
119
124
|
name,
|
|
120
|
-
options
|
|
125
|
+
options,
|
|
121
126
|
search = false,
|
|
122
127
|
autoAddBlankItem = search,
|
|
123
128
|
value,
|
|
@@ -127,15 +132,36 @@ export function SelectEx<
|
|
|
127
132
|
} = props;
|
|
128
133
|
|
|
129
134
|
// Options state
|
|
130
|
-
const [localOptions, setOptions] = React.useState(
|
|
135
|
+
const [localOptions, setOptions] = React.useState<readonly T[]>([]);
|
|
131
136
|
const isMounted = React.useRef(true);
|
|
132
137
|
|
|
138
|
+
const doItemChange = (
|
|
139
|
+
options: readonly T[],
|
|
140
|
+
value: unknown,
|
|
141
|
+
userAction: boolean
|
|
142
|
+
) => {
|
|
143
|
+
if (onItemChange == null) return;
|
|
144
|
+
if (value == null || value === '') {
|
|
145
|
+
onItemChange(undefined, userAction);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const option = options.find((option) => option[idField] === value);
|
|
149
|
+
onItemChange(option, userAction);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const setOptionsAdd = (options: readonly T[]) => {
|
|
153
|
+
setOptions(options);
|
|
154
|
+
if (localValue != null && localValue !== '')
|
|
155
|
+
doItemChange(options, localValue, false);
|
|
156
|
+
};
|
|
157
|
+
|
|
133
158
|
// When options change
|
|
134
159
|
// [options] will cause infinite loop
|
|
135
160
|
const propertyWay = loadData == null;
|
|
136
161
|
React.useEffect(() => {
|
|
137
|
-
if (
|
|
138
|
-
|
|
162
|
+
if (options == null || !propertyWay) return;
|
|
163
|
+
setOptionsAdd(options);
|
|
164
|
+
}, [options, propertyWay]);
|
|
139
165
|
|
|
140
166
|
// Local value
|
|
141
167
|
const valueSource = defaultValue ?? value ?? '';
|
|
@@ -175,11 +201,13 @@ export function SelectEx<
|
|
|
175
201
|
if (id != valueState) {
|
|
176
202
|
setValueState(id);
|
|
177
203
|
|
|
204
|
+
/*
|
|
178
205
|
const input = divRef.current?.querySelector('input');
|
|
179
206
|
if (input) {
|
|
180
207
|
// Different value, trigger change event
|
|
181
208
|
ReactUtils.triggerChange(input, id as string, false);
|
|
182
209
|
}
|
|
210
|
+
*/
|
|
183
211
|
}
|
|
184
212
|
};
|
|
185
213
|
|
|
@@ -207,7 +235,7 @@ export function SelectEx<
|
|
|
207
235
|
if (autoAddBlankItem) {
|
|
208
236
|
Utils.addBlankItem(result, idField, labelField);
|
|
209
237
|
}
|
|
210
|
-
|
|
238
|
+
setOptionsAdd(result);
|
|
211
239
|
});
|
|
212
240
|
}
|
|
213
241
|
}, [localValue]);
|
|
@@ -263,7 +291,8 @@ export function SelectEx<
|
|
|
263
291
|
multiple={multiple}
|
|
264
292
|
onChange={(event, child) => {
|
|
265
293
|
if (onChange) onChange(event, child);
|
|
266
|
-
|
|
294
|
+
doItemChange(localOptions, event.target.value, true);
|
|
295
|
+
handleChange(event);
|
|
267
296
|
}}
|
|
268
297
|
renderValue={(selected) => {
|
|
269
298
|
// The text shows up
|
|
@@ -298,7 +327,6 @@ export function SelectEx<
|
|
|
298
327
|
onItemClick(event, option);
|
|
299
328
|
if (event.defaultPrevented) return;
|
|
300
329
|
}
|
|
301
|
-
if (!multiple) setItemValue(id);
|
|
302
330
|
}}
|
|
303
331
|
style={
|
|
304
332
|
itemStyle == null
|
package/src/index.ts
CHANGED