Skip to content

Commit e4c27cb

Browse files
committed
Offer optional value prop (#22):
- deprecates optional `initialValue` prop - introduces optional `value` prop instead (default undefined) - introduces optional `clearOnClickInput` prop (default false) - introduces optional `onClick` lifecycle method prop (default empty function)
1 parent 8461c65 commit e4c27cb

File tree

10 files changed

+558
-138
lines changed

10 files changed

+558
-138
lines changed

README.md

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ 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+
2843
## Installation
2944

3045
### Installation via npm
@@ -94,9 +109,10 @@ const YourComponent = ({ myValues }) => {
94109
| [dropdownClassName](#markdown-header-dropdownClassName) | string | optional | - |
95110
| [requiredInputLength](#markdown-header-requiredInputLength) | number | optional | 0 |
96111
| [clearInputOnSelect](#markdown-header-clearInputOnSelect) | boolean | optional | false |
112+
| [clearInputOnClick](#markdown-header-clearInputOnClick) | boolean | optional | false |
97113
| [suppressReselect](#markdown-header-suppressReselect) | boolean | optional | true |
98114
| [dropDownLength](#markdown-header-dropDownLength) | number | optional | infinite |
99-
| [initialValue](#markdown-header-initialValue) | string | optional | - |
115+
| [value](#markdown-header-value) | string | optional | undefined |
100116
| [debounceTime](#markdown-header-debounceTime) | number | optional | 0 |
101117
| [debounceLoader](#markdown-header-debounceLoader) | string | optional | 'Loading...' |
102118
| [onInput](#markdown-header-onInput) | function | optional | - |
@@ -193,8 +209,15 @@ const match = (currentInput, item) =>
193209

194210
### <a name="markdown-header-clearInputOnSelect"></a>clearInputOnSelect
195211

196-
- Should the input field be cleared on select on filled with selected item?
212+
- Should the input field be cleared on select or filled with selected item?
213+
- Default is false.
214+
- ❗ 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.
215+
216+
### <a name="markdown-header-clearInputOnClick"></a>clearInputOnClick
217+
218+
- Should the input field be cleared on click or filled with selected item?
197219
- Default is false.
220+
- ❗ 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.
198221

199222
### <a name="markdown-header-suppressReselect"></a>suppressReselect
200223

@@ -207,13 +230,39 @@ const match = (currentInput, item) =>
207230
- Number to specify max length of drop down.
208231
- Default is Infinity.
209232

210-
### <a name="markdown-header-initialValue"></a>initialValue
233+
### <a name="markdown-header-value"></a>value
234+
235+
- `initialValue` is deprecated, use `value` instead
236+
- `value` can be used to specify and override the value of the input field
237+
- For example, `value="hello world"` will print `hello world` into the input field
238+
- Default is undefined
239+
- ❗ If you want to clean the input field based on side effects use `value` of empty string.
240+
- ❗ 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.
241+
- ❗ Don't confuse this with a placeholder (see placerholder prop). This property sets the actual value of the input field.
242+
- ❗ The flags `clearInputOnSelect` and `clearInputOnClick` won't work and have to be implemented via the mentioned lifecycle methods.
211243

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.
244+
The following `useEffect` is used to decide if the component should update with the new `value` property:
245+
246+
```javascript
247+
useEffect(() => {
248+
// the parent component can pass its own value prop that will override the internally used currentInput
249+
// this will happen only after we are have finished the current computing step and the dropdown is invisible
250+
// (to avoid confusion of changing input values for the user)
251+
/*
252+
* we have to distinguish undefined and empty string value
253+
* value == undefined => not set, use internal current input
254+
* value !== undefined => value set, use value and override currentInput
255+
* this enables value === '' to clear the input field
256+
*/
257+
const isValuePropSet = value !== undefined;
258+
const isValueDifferent = currentInputRef.current !== value;
259+
// is drop down visible or are we currently matching based on user input
260+
const isMatchingRunning = visible || isMatchingDebounced;
261+
if (isValuePropSet && isValueDifferent && !isMatchingRunning) {
262+
setCurrentInput(value);
263+
}
264+
}, [visible, isMatchingDebounced, value, setCurrentInput, currentInputRef]);
265+
```
217266

218267
### <a name="markdown-header-debounceTime"></a>debounceTime
219268

@@ -236,3 +285,10 @@ const match = (currentInput, item) =>
236285

237286
- The callback function that will be called whenever the user types into the input field
238287
- Exposing this function supports use cases like resetting states on empty input field
288+
- The callback will receive the `newValue` of type string from `event.target.value`
289+
290+
### <a name="markdown-header-onClick"></a>onClick
291+
292+
- The callback function that will be called whenever the user clicks the input field
293+
- This callback is exposed so you can implement `clearOnClickInput` on your own if you pass the `value` prop
294+
- 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> {

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)