Skip to content

Commit 82aae40

Browse files
Merge pull request #24 from andrelandgraf/fix/emptyInitalValue-23
Fix empty initial value - Offer optional value prop
2 parents 8461c65 + b926ed0 commit 82aae40

File tree

11 files changed

+567
-139
lines changed

11 files changed

+567
-139
lines changed

README.md

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@ I created a plain version of this package without css. Find more information [he
2525

2626
The documentation below mainly applies for both versions but will be updated based on version 2.x.x updates in the future.
2727

28+
### Changelog
29+
30+
#### Version 2.1.0
31+
32+
Motivation: [issue 23](https://github.com/andrelandgraf/react-datalist-input/issues/23)
33+
34+
Offer optional value prop, in case the user requires full control to change/clear the input value based on side effects
35+
36+
Changes:
37+
38+
- deprecates optional `initialValue` prop
39+
- introduces optional `value` prop instead (default undefined)
40+
- introduces optional `clearOnClickInput` prop (default false)
41+
- introduces optional `onClick` lifecycle method prop (default empty function)
42+
43+
#### Version 2.0.0
44+
45+
Changes:
46+
47+
- refactors component to functional component using hooks
48+
- adds `useStateRef` to reduce re-renders and boost performance
49+
2850
## Installation
2951

3052
### Installation via npm
@@ -94,12 +116,14 @@ const YourComponent = ({ myValues }) => {
94116
| [dropdownClassName](#markdown-header-dropdownClassName) | string | optional | - |
95117
| [requiredInputLength](#markdown-header-requiredInputLength) | number | optional | 0 |
96118
| [clearInputOnSelect](#markdown-header-clearInputOnSelect) | boolean | optional | false |
119+
| [clearInputOnClick](#markdown-header-clearInputOnClick) | boolean | optional | false |
97120
| [suppressReselect](#markdown-header-suppressReselect) | boolean | optional | true |
98121
| [dropDownLength](#markdown-header-dropDownLength) | number | optional | infinite |
99-
| [initialValue](#markdown-header-initialValue) | string | optional | - |
122+
| [value](#markdown-header-value) | string | optional | undefined |
100123
| [debounceTime](#markdown-header-debounceTime) | number | optional | 0 |
101124
| [debounceLoader](#markdown-header-debounceLoader) | string | optional | 'Loading...' |
102125
| [onInput](#markdown-header-onInput) | function | optional | - |
126+
| [onClick](#markdown-header-onClick) | function | optional | - |
103127

104128
### <a name="markdown-header-items"></a>items
105129

@@ -193,8 +217,15 @@ const match = (currentInput, item) =>
193217

194218
### <a name="markdown-header-clearInputOnSelect"></a>clearInputOnSelect
195219

196-
- Should the input field be cleared on select on filled with selected item?
220+
- Should the input field be cleared on select or filled with selected item?
197221
- Default is false.
222+
- ❗ This property does not work if the prop `value` is set, you have to use the lifecycle method `onSelect` and set your value state on your own.
223+
224+
### <a name="markdown-header-clearInputOnClick"></a>clearInputOnClick
225+
226+
- Should the input field be cleared on click or filled with selected item?
227+
- Default is false.
228+
- ❗ This property does not workif the prop `value` is set, you have to use the lifecycle method `onClick` and set your value state on your own.
198229

199230
### <a name="markdown-header-suppressReselect"></a>suppressReselect
200231

@@ -207,13 +238,39 @@ const match = (currentInput, item) =>
207238
- Number to specify max length of drop down.
208239
- Default is Infinity.
209240

210-
### <a name="markdown-header-initialValue"></a>initialValue
241+
### <a name="markdown-header-value"></a>value
242+
243+
- `initialValue` is deprecated, use `value` instead
244+
- `value` can be used to specify and override the value of the input field
245+
- For example, `value="hello world"` will print `hello world` into the input field
246+
- Default is undefined
247+
- ❗ If you want to clean the input field based on side effects use `value` of empty string.
248+
- ❗ Use `value` only if you want complete control over the value of the input field. `react-datalist-input` will priotize whatever value is set over anything the user selects or has selected. If you use `value`, you will have to update it on your own using the `onClick`, `onInput`, and`onSelect` lifecycle methods.
249+
- ❗ Don't confuse this with a placeholder (see placerholder prop). This property sets the actual value of the input field.
250+
- ❗ The flags `clearInputOnSelect` and `clearInputOnClick` won't work and have to be implemented via the mentioned lifecycle methods.
211251

212-
- Specify an initial value for the input field.
213-
- For example, `initialValue={'hello world'}` will print `hello world` into the input field on first render.
214-
- Default is empty string.
215-
- Caution: Don't confuse this with a placeholder (see placerholder prop), this is an actual value in the input
216-
and supports uses cases like saving user state or suggesting a search value.
252+
The following `useEffect` is used to decide if the component should update with the new `value` property:
253+
254+
```javascript
255+
useEffect(() => {
256+
// the parent component can pass its own value prop that will override the internally used currentInput
257+
// this will happen only after we are have finished the current computing step and the dropdown is invisible
258+
// (to avoid confusion of changing input values for the user)
259+
/*
260+
* we have to distinguish undefined and empty string value
261+
* value == undefined => not set, use internal current input
262+
* value !== undefined => value set, use value and override currentInput
263+
* this enables value === '' to clear the input field
264+
*/
265+
const isValuePropSet = value !== undefined;
266+
const isValueDifferent = currentInputRef.current !== value;
267+
// is drop down visible or are we currently matching based on user input
268+
const isMatchingRunning = visible || isMatchingDebounced;
269+
if (isValuePropSet && isValueDifferent && !isMatchingRunning) {
270+
setCurrentInput(value);
271+
}
272+
}, [visible, isMatchingDebounced, value, setCurrentInput, currentInputRef]);
273+
```
217274

218275
### <a name="markdown-header-debounceTime"></a>debounceTime
219276

@@ -236,3 +293,10 @@ const match = (currentInput, item) =>
236293

237294
- The callback function that will be called whenever the user types into the input field
238295
- Exposing this function supports use cases like resetting states on empty input field
296+
- The callback will receive the `newValue` of type string from `event.target.value`
297+
298+
### <a name="markdown-header-onClick"></a>onClick
299+
300+
- The callback function that will be called whenever the user clicks the input field
301+
- This callback is exposed so you can implement `clearOnClickInput` on your own if you pass the `value` prop
302+
- The callback will receive the `currentInput` of type string based on `clearOnClickInput` and the last user input

index.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ declare module 'react-datalist-input' {
1818
dropdownClassName?: string;
1919
requiredInputLength?: number;
2020
clearInputOnSelect?: boolean;
21+
clearInputOnClick?: boolean;
2122
suppressReselect?: boolean;
2223
dropDownLength?: number;
23-
initialValue?: string;
24+
value?: string;
2425
onDropdownOpen?: () => void;
2526
onDropdownClose?: () => void;
2627
debounceTime?: number;
2728
debounceLoader?: React.ReactNode;
2829
onInput?: (inputValue: string) => void;
30+
onClick?: (inputValue: string) => void;
2931
}
3032

3133
export default class DataListInput extends React.Component<DataListInputProperties> {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-datalist-input",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "This package provides a react component as follows: an input field with a drop down menu to pick a possible option based on the current input.",
55
"main": "./dist/DataListInput.js",
66
"files": [

src/DataListInput.jsx

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,20 @@ const indexOfItem = (item, items) =>
4242
const DataListInput = ({
4343
activeItemClassName,
4444
clearInputOnSelect,
45+
clearInputOnClick,
4546
debounceLoader,
4647
debounceTime,
4748
dropdownClassName,
4849
dropDownLength,
49-
initialValue,
50+
value,
5051
inputClassName,
5152
itemClassName,
5253
match,
5354
onDropdownClose,
5455
onDropdownOpen,
5556
onInput,
5657
onSelect,
58+
onClick,
5759
placeholder,
5860
requiredInputLength,
5961
suppressReselect,
@@ -63,7 +65,7 @@ const DataListInput = ({
6365
const [lastValidItem, setLastValidItem] = useState();
6466
/* current input text */
6567
const [currentInput, setCurrentInput, currentInputRef] = useStateRef(
66-
initialValue
68+
value !== undefined ? value : ''
6769
);
6870
/* current set of matching items */
6971
const [matchingItems, setMatchingItems] = useState([]);
@@ -113,18 +115,22 @@ const DataListInput = ({
113115
}, [onDropdownClose, setVisible, visibleRef]);
114116

115117
useEffect(() => {
116-
// if we have an initialValue, we want to reset it everytime we update and are empty
117-
// also setting a new initialValue will trigger this
118-
if (!currentInput && initialValue && !visible && !isMatchingDebounced) {
119-
setCurrentInput(initialValue);
118+
// the parent component can pass its own value prop that will override the internally used currentInput
119+
// this will happen only after we are have finished the current computing step and the dropdown is invisible
120+
// (to avoid confusion of changing input values for the user)
121+
/*
122+
* we have to distinguish undefined and empty string value
123+
* value == undefined => not set, use internal current input
124+
* value !== undefined => value set, use value and override currentInput
125+
* this enables value === '' to clear the input field
126+
*/
127+
const isValuePropSet = value !== undefined;
128+
const isValueDifferent = currentInputRef.current !== value;
129+
const isMatchingRunning = visible || isMatchingDebounced;
130+
if (isValuePropSet && isValueDifferent && !isMatchingRunning) {
131+
setCurrentInput(value);
120132
}
121-
}, [
122-
currentInput,
123-
visible,
124-
isMatchingDebounced,
125-
initialValue,
126-
setCurrentInput,
127-
]);
133+
}, [visible, isMatchingDebounced, value, setCurrentInput, currentInputRef]);
128134

129135
/**
130136
* runs the matching process of the current input
@@ -206,32 +212,33 @@ const DataListInput = ({
206212

207213
/**
208214
* gets called when someone starts to write in the input field
209-
* @param value
215+
* @param event
210216
*/
211217
const onHandleInput = useCallback(
212218
event => {
213-
const { value } = event.target;
214-
debouncedMatchingUpdateStep(value);
215-
onInput(value);
219+
const { value: newValue } = event.target;
220+
debouncedMatchingUpdateStep(newValue);
221+
onInput(newValue);
216222
},
217223
[debouncedMatchingUpdateStep, onInput]
218224
);
219225

220226
const onClickInput = useCallback(() => {
221-
let value = currentInputRef.current;
222-
// if user clicks on input field with initialValue,
227+
let currentValue = currentInputRef.current;
228+
// if user clicks on input field with value,
223229
// the user most likely wants to clear the input field
224-
if (initialValue && value === initialValue) {
225-
value = '';
230+
if (currentValue && clearInputOnClick) {
231+
currentValue = '';
226232
}
227-
228-
const reachedRequiredLength = value.length >= requiredInputLength;
233+
onClick(currentValue);
234+
const reachedRequiredLength = currentValue.length >= requiredInputLength;
229235
if (reachedRequiredLength && !visibleRef.current) {
230-
debouncedMatchingUpdateStep(value);
236+
debouncedMatchingUpdateStep(currentValue);
231237
}
232238
}, [
233239
currentInputRef,
234-
initialValue,
240+
clearInputOnClick,
241+
onClick,
235242
requiredInputLength,
236243
visibleRef,
237244
debouncedMatchingUpdateStep,
@@ -448,12 +455,14 @@ DataListInput.propTypes = {
448455
activeItemClassName: PropTypes.string,
449456
requiredInputLength: PropTypes.number,
450457
clearInputOnSelect: PropTypes.bool,
458+
clearInputOnClick: PropTypes.bool,
451459
suppressReselect: PropTypes.bool,
452460
dropDownLength: PropTypes.number,
453-
initialValue: PropTypes.string,
461+
value: PropTypes.string,
454462
debounceTime: PropTypes.number,
455463
debounceLoader: PropTypes.node,
456464
onInput: PropTypes.func,
465+
onClick: PropTypes.func,
457466
};
458467

459468
DataListInput.defaultProps = {
@@ -465,14 +474,16 @@ DataListInput.defaultProps = {
465474
activeItemClassName: '',
466475
requiredInputLength: 0,
467476
clearInputOnSelect: false,
477+
clearInputOnClick: false,
468478
suppressReselect: true,
469479
dropDownLength: Infinity,
470-
initialValue: '',
480+
value: undefined,
471481
debounceTime: 0,
472482
debounceLoader: undefined,
473483
onDropdownOpen: () => {},
474484
onDropdownClose: () => {},
475485
onInput: () => {},
486+
onClick: () => {},
476487
};
477488

478489
export default DataListInput;

tests/demo-app/.vscode/launch.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"name": "vscode-jest-tests",
10+
"request": "launch",
11+
"args": [
12+
"--runInBand"
13+
],
14+
"cwd": "${workspaceFolder}",
15+
"console": "integratedTerminal",
16+
"internalConsoleOptions": "neverOpen",
17+
"disableOptimisticBPs": true,
18+
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
19+
},
20+
{
21+
"name": "Chrome",
22+
"type": "chrome",
23+
"request": "launch",
24+
"url": "http://localhost:3000",
25+
"webRoot": "${workspaceFolder}/src",
26+
"sourceMapPathOverrides": {
27+
"webpack:///src/*": "${webRoot}/*"
28+
}
29+
}
30+
]
31+
}

0 commit comments

Comments
 (0)