Skip to content

Add useDebounce hook and timeoutOnChange for TextField #3365

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 12 additions & 25 deletions src/components/textField/textField.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,9 @@
"name": "TextField",
"category": "form",
"description": "An enhanced customizable TextField with validation support",
"extends": [
"TextInput"
],
"extendsLink": [
"https://reactnative.dev/docs/textinput"
],
"modifiers": [
"margin",
"color",
"typography"
],
"extends": ["TextInput"],
"extendsLink": ["https://reactnative.dev/docs/textinput"],
"modifiers": ["margin", "color", "typography"],
"example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TextFieldScreen.tsx",
"images": [
"https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/FloatingPlaceholder.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/Validation.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/ColorByState.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/CharCounter.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Incubator.TextField/Hint.gif?raw=true"
Expand Down Expand Up @@ -149,6 +141,11 @@
"type": "boolean",
"description": "Should validate when losing focus of TextField"
},
{
"name": "validationDebounceTime",
"type": "number",
"description": "Add a debounce timeout when sending validateOnChange"
},
{
"name": "onChangeValidity",
"type": "(isValid: boolean) => void",
Expand Down Expand Up @@ -288,11 +285,7 @@
},
{
"type": "table",
"columns": [
"Property",
"Component",
"New Column"
],
"columns": ["Property", "Component", "New Column"],
"items": [
{
"title": "Inactive (default)",
Expand Down Expand Up @@ -381,10 +374,7 @@
},
{
"type": "table",
"columns": [
"Property",
"Component"
],
"columns": ["Property", "Component"],
"items": [
{
"title": "Outline (Default)",
Expand All @@ -410,10 +400,7 @@
},
{
"type": "table",
"columns": [
"Property",
"Component"
],
"columns": ["Property", "Component"],
"items": [
{
"title": "Leading Accessory (Prefix)",
Expand Down Expand Up @@ -510,7 +497,7 @@
"type": "section",
"layout": "horizontal",
"title": "Trailing Accessory (Suffix) - Icon",
"description": "Use an icon as the trailing accessory to help users understand or access extra features related to the input.The icon can be placed either in line with the label (top trailing accessory), or as part of the input field.\n\nInclude an information icon for users to access additional details about the required input. When clicked, the icon can activate a Hint component for brief information or a Dialog for more detailed explanations.\n\nIcon suffix can be used to toggle between 2 states, e.g. hiding/showing the input value. Or to indicate a success validation state.",
"description": "Use an icon as the trailing accessory to help users understand or access extra features related to the input.The icon can be placed either in line with the label (top trailing accessory), or as part of the input field.\n\nInclude an information icon for users to access additional details about the required input. When clicked, the icon can activate a Hint component for brief information or a Dialog for more detailed explanations.\n\nIcon suffix can be used to toggle between 2 states, e.g. hiding/showing the input value. Or to indicate a success validation state.",
"content": [
{
"value": "https://wixmp-1d257fba8470f1b562a0f5f2.wixmp.com/mads-docs-assets/assets/Components Docs/textField/textField_uxguidelines_section_trailingIcon.png"
Expand Down
5 changes: 5 additions & 0 deletions src/components/textField/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type Validator = ((value?: string) => boolean) | keyof typeof formValidat
export interface FieldStateProps extends InputProps {
validateOnStart?: boolean;
validateOnChange?: boolean;
validationDebounceTime?: number;
validateOnBlur?: boolean;
/**
* Callback for when field validated and failed
Expand Down Expand Up @@ -241,6 +242,10 @@ export type TextFieldProps = MarginModifiers &
* Should validate when the TextField value changes
*/
validateOnChange?: boolean;
/**
* Add a debounce timeout when sending validateOnChange
*/
validationDebounceTime?: number;
/**
* Should validate when losing focus of TextField
*/
Expand Down
47 changes: 29 additions & 18 deletions src/components/textField/useFieldState.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {useCallback, useState, useEffect, useMemo} from 'react';
import _ from 'lodash';
import * as Presenter from './Presenter';
import {useDidUpdate} from 'hooks';
import {useDidUpdate, useDebounce} from 'hooks';
import {FieldStateProps} from './types';
import {Constants} from '../../commons/new';

Expand All @@ -10,6 +10,7 @@ export default function useFieldState({
validationMessage,
validateOnBlur,
validateOnChange,
validationDebounceTime,
validateOnStart,
onValidationFailed,
onChangeValidity,
Expand Down Expand Up @@ -42,12 +43,32 @@ export default function useFieldState({
}
}, []);

const validateField = useCallback((valueToValidate = value) => {
const [_isValid, _failingValidatorIndex] = Presenter.validate(valueToValidate, validate);

setIsValid(_isValid);
setFailingValidatorIndex(_failingValidatorIndex);

if (!_isValid && !_.isUndefined(_failingValidatorIndex)) {
onValidationFailed?.(_failingValidatorIndex);
}

return _isValid;
},
[value, validate, onValidationFailed]);

const debouncedValidateField = useDebounce(validateField, validationDebounceTime);

useEffect(() => {
if (propsValue !== value) {
setValue(propsValue);

if (validateOnChange) {
validateField(propsValue);
if (validationDebounceTime) {
debouncedValidateField(propsValue);
} else {
validateField(propsValue);
}
}
}
/* On purpose listen only to propsValue change */
Expand All @@ -65,20 +86,6 @@ export default function useFieldState({
return _isValid;
}, [value, validate]);

const validateField = useCallback((valueToValidate = value) => {
const [_isValid, _failingValidatorIndex] = Presenter.validate(valueToValidate, validate);

setIsValid(_isValid);
setFailingValidatorIndex(_failingValidatorIndex);

if (!_isValid && !_.isUndefined(_failingValidatorIndex)) {
onValidationFailed?.(_failingValidatorIndex);
}

return _isValid;
},
[value, validate, onValidationFailed]);

const onFocus = useCallback((...args: any) => {
setIsFocused(true);
//@ts-expect-error
Expand All @@ -101,10 +108,14 @@ export default function useFieldState({
props.onChangeText?.(text);

if (validateOnChange) {
validateField(text);
if (validationDebounceTime) {
debouncedValidateField(text);
} else {
validateField(text);
}
}
},
[props.onChangeText, validateOnChange, validateField]);
[props.onChangeText, validateOnChange, debouncedValidateField, validateField]);

const fieldState = useMemo(() => {
return {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export {default as useScrollReached} from './useScrollReached';
export {default as useScrollToItem} from './useScrollToItem';
export {default as useScrollTo} from './useScrollTo';
export {default as useThemeProps} from './useThemeProps';
export {default as useDebounce} from './useDebounce';
export * from './useScrollTo';

20 changes: 20 additions & 0 deletions src/hooks/useDebounce/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {useCallback, useRef} from 'react';

/**
* This hook is used to debounce a function call
*/
function useDebounce<A>(func: (args: A) => void, timeout = 300) {
const handler = useRef<NodeJS.Timeout>();
const debouncedFunction = useCallback((args: A) => {
if (handler.current) {
clearTimeout(handler.current);
}
handler.current = setTimeout(() => {
func(args);
}, timeout);
}, [func, timeout]);

return debouncedFunction;
}

export default useDebounce;