Skip to content
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

feat: added useMaxlength hook #2516

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
- [`useRendersCount`](./docs/useRendersCount.md) — count component renders. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-userenderscount--demo)
- [`createGlobalState`](./docs/createGlobalState.md) — cross component shared state.[![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-createglobalstate--demo)
- [`useMethods`](./docs/useMethods.md) — neat alternative to `useReducer`. [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemethods--demo)
- [`useMaxlength`](./docs/useMaxlength.md) — handles the max-length property of an input or textarea or any other use case where a max-length is needed [![][img-demo]](https://streamich.github.io/react-use/?path=/story/state-usemaxlength--demo)
<br/>
<br/>
- [**Miscellaneous**]()
Expand Down
96 changes: 96 additions & 0 deletions docs/useMaxlength.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# `useMaxlength`

Custom React Hook to handle the max-length property of an input or textarea or any other use case where a max-length is needed. This hook provides additional features such as a threshold for warnings and UTF-8 support.

## Usage

```jsx
import { useMaxlength } from 'react-use';

const Demo = () => {
const { currentValue, onChange } = useMaxlength({
maxLength: 10,
counterThreshold: 5,
utf8: true,
validate: true,
warningThreshold: 2,
initialValue: 'Hello'
});

return (
<input
value={currentValue}
onChange={(event) => onChange(event.target.value)}
maxLength={10}
/>
);
};
```

# Parameter Details

The hook `useMaxlength` takes an object which can contain following properties;

## counterThreshold

Optional. A number. How many remaining characters trigger the counter. Default is 0.

## warningThreshold

Optional. A number. How many remaining characters trigger the warning state. The value must be lower than the counterThreshold and the maxLength. If not set, the warning state is never triggered. Default is 0.

## validate

Optional. A boolean. If true, prevent more characters than max-length when not supported by the browser. Default is false.

## utf8

Optional. A boolean. If true, count characters by byte-size rather than length (eg: £ is counted as two characters). Default is false.

## maxLength

Required. A number. Maximum amount of characters allowed to be entered in the input.

## initialValue

Optional. A string. The initial value of the input. Default is ''.

# Returns

The hook `useMaxlength` returns an object with the following properties;

## isWarning

A boolean. True if the input value length reaches the warning threshold.

## isLimitReached

A boolean. True if the input value length reaches or exceeds the max-length.

## isLimitExceeded

A boolean. True if the input value length exceeds the max-length.

## counter

A number. Current length of the input value.

## isShowCounter

A boolean. True if the count should be displayed.

## charactersLeft

A number. How many characters are left before reaching the max-length.

## maxLength

The max-length value passed to the hook.

## currentValue

The formatted value of the input.

## onChange

A function. Event handler for the input's onChange event. It should be passed to the input field as onChange handler.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,4 @@ export { useFirstMountState } from './useFirstMountState';
export { default as useSet } from './useSet';
export { createGlobalState } from './factory/createGlobalState';
export { useHash } from './useHash';
export { default as useMaxlength } from './useMaxlength';
120 changes: 120 additions & 0 deletions src/useMaxlength.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
/**
* Custom React Hook to handle the max-length property of an input or textarea or any other use case where a max-length is needed.
* Provides additional features such as a threshold for warnings and UTF-8 support.
*
* @typedef {Object} IUseMaxlengthInput
* @property {number} [counterThreshold=0] - How many remaining characters trigger the counter.
* @property {number} [warningThreshold=0] - How many remaining characters trigger the warning state. If not set, the warning state is never triggered. The value must be lower than the counterThreshold and the maxLength.
* @property {boolean} [validate=false] - If true, prevent more characters than max-length when not supported by the browser.
* @property {boolean} [utf8=false] - If true, count characters by byte-size rather than length (eg: £ is counted as two characters).
* @property {number} [maxLength] - Maximum amount of characters allowed to be entered in the input.
* @property {string} [initialValue=''] - The initial value of the input.
*/
export interface IUseMaxlengthInput {
counterThreshold?: number;
validate?: boolean;
utf8?: boolean;
maxLength: number;
initialValue?: string;
warningThreshold?: number;
}

/**
* @typedef {Object} IUseMaxlengthReturn
* @property {boolean} isWarning - True if the input value length reaches the threshold.
* @property {boolean} isLimitReached - True if the input value length reaches or exceeds the max-length.
* @property {boolean} isLimitExceeded - True if the input value length exceeds the max-length.
* @property {number} counter - Current length of the input value.
* @property {boolean} isShowCounter - True if the counter should be displayed.
* @property {number} charactersLeft - How many characters are left before reaching the max-length.
* @property {number} [maxLength] - The max-length value passed to the hook.
* @property {function} onChange - Event handler for the input\'s onChange event.
* @property {string} currentValue - The formatted value of the input.
*/
export interface IUseMaxlengthReturn {
isWarning: boolean;
isLimitReached: boolean;
isLimitExceeded: boolean;
counter: number;
isShowCounter: boolean;
charactersLeft: number;
maxLength: number;
onChange: (value: string) => void;
currentValue: string;
}

/**
* Implement and return the behaviour of an input that enforces a max-length.
*
* @param {IUseMaxlengthInput} param0
* @returns {IUseMaxlengthReturn}
*/
const useMaxlength = ({
counterThreshold = 0,
validate = false,
utf8 = false,
maxLength,
initialValue = '',
warningThreshold = 0,
}: IUseMaxlengthInput): IUseMaxlengthReturn => {
if (maxLength <= 0) {
throw new Error('maxLength must be a positive number');
}

if (counterThreshold < 0 || counterThreshold > maxLength) {
throw new Error('counterThreshold must be a positive number and less than maxLength');
}

if (warningThreshold < 0 || warningThreshold > maxLength) {
throw new Error('warningThreshold must be a positive number and less than maxLength');
}

const [currentValue, setCurrentValue] = useState('');
const [counter, setCounter] = useState(0);

const { isLimitReached, isLimitExceeded, isWarning, isShowCounter, charactersLeft } =
useMemo(() => {
return {
isLimitReached: counter >= maxLength,
isLimitExceeded: counter > maxLength,
isWarning: counter >= maxLength - warningThreshold,
isShowCounter: counter >= counterThreshold,
charactersLeft: maxLength - counter,
};
}, [counter, maxLength, warningThreshold, counterThreshold]);

const onChange = useCallback(
(inputValue: string) => {
let newValue = inputValue;

const length = utf8 ? encodeURI(inputValue).split(/%..|./).length - 1 : inputValue.length;

if (validate && length > maxLength) {
newValue = inputValue.slice(0, maxLength);
}

setCounter(length);
setCurrentValue(newValue);
},
[utf8, validate, maxLength]
);

useEffect(() => {
initialValue && onChange(initialValue);
}, [initialValue]);

return {
isWarning,
isLimitReached,
isLimitExceeded,
counter,
isShowCounter,
onChange,
maxLength,
charactersLeft,
currentValue,
};
};

export default useMaxlength;
101 changes: 101 additions & 0 deletions stories/useMaxlength.story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { storiesOf } from '@storybook/react';
import * as React from 'react';
import { useMaxlength } from '../src';
import { IUseMaxlengthInput } from '../src/useMaxlength';
import ShowDocs from './util/ShowDocs';

const DemoRequire = () => {
const maxlengthInput: IUseMaxlengthInput = {
counterThreshold: 10,
validate: true,
utf8: false,
maxLength: 50,
initialValue: 'Sample text',
warningThreshold: 5,
};

const { isWarning, counter, isShowCounter, onChange, maxLength, charactersLeft, currentValue } =
useMaxlength(maxlengthInput);

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};

return (
<div>
<input type="text" value={currentValue} onChange={handleInputChange} maxLength={maxLength} />
{isShowCounter && (
<div className={`counter ${isWarning ? 'warning' : ''}`}>
Counter: {counter}, Characters left: {charactersLeft}
</div>
)}
</div>
);
};

storiesOf('State/useMaxlength', module)
.add('Docs', () => <ShowDocs md={require('../docs/useMaxlength.md')} />)
.add('Demo', () => <DemoRequire />)
.add('Threshold not reached', () => (
<CounterTester maxLength={20} initialValue="Hello" counterThreshold={10} />
))
.add('Threshold reached', () => (
<CounterTester maxLength={20} initialValue="Hello World" counterThreshold={10} />
))
.add('Max length reached', () => (
<CounterTester initialValue="Hello World, How are you doing today?" maxLength={30} />
))
.add('Max length exceeded', () => (
<CounterTester
initialValue="Hello World, How are you doing today? I am doing good."
maxLength={50}
validate={true}
/>
))
.add('UTF-8 characters', () => (
<CounterTester maxLength={20} initialValue="Hello £" utf8={true} />
));

const CounterTester = ({
initialValue,
counterThreshold,
maxLength,
validate,
utf8,
warningThreshold,
}: IUseMaxlengthInput) => {
const {
counter,
isShowCounter,
onChange,
charactersLeft,
currentValue,
isWarning,
isLimitReached,
isLimitExceeded,
maxLength: maxLengthHook,
} = useMaxlength({ counterThreshold, maxLength, validate, utf8, warningThreshold, initialValue });

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value);
};

return (
<div>
<input
type="text"
value={currentValue}
onChange={handleInputChange}
maxLength={maxLengthHook}
/>
{isLimitExceeded ? <div className="error">Limit Exceeded</div> : ''}
{isLimitReached ? <div className="warning">Limit Reached</div> : ''}
{isWarning ? <div className="warning">Almost There</div> : ''}
{isShowCounter && (
<div className={`counter ${isWarning ? 'warning' : ''}`}>
Counter: {counter}, Characters left: {charactersLeft}
</div>
)}
</div>
);
};