-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# `useMultiStateValidator` | ||
|
||
Each time any of given states changes - validator function is invoked. | ||
|
||
## Usage | ||
```ts | ||
import * as React from 'react'; | ||
import { useMultiStateValidator } from 'react-use'; | ||
|
||
const DemoStateValidator = (s: number[]) => [s.every((num: number) => !(num % 2))] as [boolean]; | ||
const Demo = () => { | ||
const [state1, setState1] = React.useState<number>(1); | ||
const [state2, setState2] = React.useState<number>(1); | ||
const [state3, setState3] = React.useState<number>(1); | ||
const [[isValid]] = useMultiStateValidator([state1, state2, state3], DemoStateValidator); | ||
|
||
return ( | ||
<div> | ||
<div>Below fields will be valid if all of them is even</div> | ||
<input type="number" min="1" max="10" value={state1} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState1((ev.target.value as unknown) as number); | ||
}} | ||
/> | ||
<input type="number" min="1" max="10" value={state2} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState2((ev.target.value as unknown) as number); | ||
}} | ||
/> | ||
<input type="number" min="1" max="10" value={state3} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState3((ev.target.value as unknown) as number); | ||
}} | ||
/> | ||
{isValid !== null && <span>{isValid ? 'Valid!' : 'Invalid'}</span>} | ||
</div> | ||
); | ||
}; | ||
``` | ||
|
||
## Reference | ||
```ts | ||
const [validity, revalidate] = useStateValidator( | ||
state: any[] | { [p: string]: any } | { [p: number]: any }, | ||
validator: (state, setValidity?)=>[boolean|null, ...any[]], | ||
initialValidity: any = [undefined] | ||
); | ||
``` | ||
- **`state`**_`: any[] | { [p: string]: any } | { [p: number]: any }`_ can be both an array or object. It's _values_ will be used as a deps for inner `useEffect`. | ||
- **`validity`**_`: [boolean|null, ...any[]]`_ result of validity check. First element is strictly nullable boolean, but others can contain arbitrary data; | ||
- **`revalidate`**_`: ()=>void`_ runs validator once again | ||
- **`validator`**_`: (state, setValidity?)=>[boolean|null, ...any[]]`_ should return an array suitable for validity state described above; | ||
- `states` - current states values as the've been passed to the hook; | ||
- `setValidity` - if defined hook will not trigger validity change automatically. Useful for async validators; | ||
- `initialValidity` - validity value which set when validity is nt calculated yet; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { storiesOf } from '@storybook/react'; | ||
import * as React from 'react'; | ||
import { useMultiStateValidator } from '../'; | ||
import ShowDocs from './util/ShowDocs'; | ||
|
||
const DemoStateValidator = (s: number[]) => [s.every((num: number) => !(num % 2))] as [boolean]; | ||
const Demo = () => { | ||
const [state1, setState1] = React.useState<number>(1); | ||
const [state2, setState2] = React.useState<number>(1); | ||
const [state3, setState3] = React.useState<number>(1); | ||
const [[isValid]] = useMultiStateValidator([state1, state2, state3], DemoStateValidator); | ||
|
||
return ( | ||
<div> | ||
<div>Below fields will be valid if all of them is even</div> | ||
<br /> | ||
<input | ||
type="number" | ||
min="1" | ||
max="10" | ||
value={state1} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState1((ev.target.value as unknown) as number); | ||
}} | ||
/> | ||
<input | ||
type="number" | ||
min="1" | ||
max="10" | ||
value={state2} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState2((ev.target.value as unknown) as number); | ||
}} | ||
/> | ||
<input | ||
type="number" | ||
min="1" | ||
max="10" | ||
value={state3} | ||
onChange={(ev: React.ChangeEvent<HTMLInputElement>) => { | ||
setState3((ev.target.value as unknown) as number); | ||
}} | ||
/> | ||
{isValid !== null && <span style={{ marginLeft: 24 }}>{isValid ? 'Valid!' : 'Invalid'}</span>} | ||
</div> | ||
); | ||
}; | ||
|
||
storiesOf('State|useMultiStateValidator', module) | ||
.add('Docs', () => <ShowDocs md={require('../../docs/useMultiStateValidator.md')} />) | ||
.add('Demo', () => <Demo />); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks'; | ||
import { useState } from 'react'; | ||
import { MultiStateValidator, useMultiStateValidator } from '../useMultiStateValidator'; | ||
import { UseValidatorReturn, ValidityState } from '../useStateValidator'; | ||
|
||
interface Mock extends jest.Mock {} | ||
|
||
describe('useMultiStateValidator', () => { | ||
it('should be defined', () => { | ||
expect(useMultiStateValidator).toBeDefined(); | ||
}); | ||
|
||
const defaultStatesValidator = (states: number[]) => [states.every(num => !(num % 2))]; | ||
|
||
function getHook( | ||
fn: MultiStateValidator<any, number[]> = jest.fn(defaultStatesValidator), | ||
initialStates = [1, 2], | ||
initialValidity = [false] | ||
): [MultiStateValidator<any, number[]>, RenderHookResult<any, [Function, UseValidatorReturn<ValidityState>]>] { | ||
return [ | ||
fn, | ||
renderHook( | ||
({ initStates, validator, initValidity }) => { | ||
const [states, setStates] = useState(initStates); | ||
|
||
return [setStates, useMultiStateValidator(states, validator, initValidity)]; | ||
}, | ||
{ | ||
initialProps: { | ||
initStates: initialStates, | ||
initValidity: initialValidity, | ||
validator: fn, | ||
}, | ||
} | ||
), | ||
]; | ||
} | ||
|
||
it('should return an array of two elements', () => { | ||
const [, hook] = getHook(); | ||
const res = hook.result.current[1]; | ||
|
||
expect(Array.isArray(res)).toBe(true); | ||
expect(res.length).toBe(2); | ||
}); | ||
|
||
it('should call validator on init', () => { | ||
const [spy] = getHook(); | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should call validator on any of states changed', () => { | ||
const [spy, hook] = getHook(); | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
act(() => hook.result.current[0]([4, 2])); | ||
expect(spy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it("should NOT call validator on it's change", () => { | ||
const [spy, hook] = getHook(); | ||
const newValidator: MultiStateValidator<any, number[]> = jest.fn(states => [states!.every(num => !!(num % 2))]); | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
hook.rerender({ validator: newValidator }); | ||
expect(spy).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should throw if states is not an object', () => { | ||
try { | ||
// @ts-ignore | ||
getHook(defaultStatesValidator, 123); | ||
} catch (err) { | ||
expect(err).toBeDefined(); | ||
expect(err instanceof Error).toBe(true); | ||
expect(err.message).toBe('states expected to be an object or array, got number'); | ||
} | ||
}); | ||
|
||
it('first returned element should represent current validity state', () => { | ||
const [, hook] = getHook(); | ||
let [setState, [validity]] = hook.result.current; | ||
expect(validity).toEqual([false]); | ||
|
||
act(() => setState([4, 2])); | ||
[setState, [validity]] = hook.result.current; | ||
expect(validity).toEqual([true]); | ||
|
||
act(() => setState([4, 5])); | ||
[setState, [validity]] = hook.result.current; | ||
expect(validity).toEqual([false]); | ||
}); | ||
|
||
it('second returned element should re-call validation', () => { | ||
const [spy, hook] = getHook(); | ||
const [, [, revalidate]] = hook.result.current; | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
act(() => revalidate()); | ||
expect(spy).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('validator should receive states as a firs argument', () => { | ||
const [spy, hook] = getHook(); | ||
const [setState] = hook.result.current; | ||
|
||
expect((spy as Mock).mock.calls[0].length).toBe(1); | ||
expect((spy as Mock).mock.calls[0][0]).toEqual([1, 2]); | ||
act(() => setState([4, 6])); | ||
expect((spy as Mock).mock.calls[1][0]).toEqual([4, 6]); | ||
}); | ||
|
||
it('if validator expects 2nd parameters it should pass a validity setter there', () => { | ||
const spy = (jest.fn((states: number[], done) => { | ||
done([states.every(num => !!(num % 2))]); | ||
}) as unknown) as MultiStateValidator; | ||
const [, hook] = getHook(spy, [1, 3]); | ||
const [, [validity]] = hook.result.current; | ||
|
||
expect((spy as Mock).mock.calls[0].length).toBe(2); | ||
expect((spy as Mock).mock.calls[0][0]).toEqual([1, 3]); | ||
expect(validity).toEqual([true]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { useCallback, useEffect, useRef, useState } from 'react'; | ||
import { DispatchValidity, UseValidatorReturn, ValidityState } from './useStateValidator'; | ||
|
||
export type MultiStateValidatorStates = any[] | { [p: string]: any } | { [p: number]: any }; | ||
|
||
export interface MultiStateValidator< | ||
V extends ValidityState = ValidityState, | ||
S extends MultiStateValidatorStates = MultiStateValidatorStates | ||
> { | ||
(states: S): V; | ||
|
||
(states: S, done: DispatchValidity<V>): void; | ||
} | ||
|
||
export function useMultiStateValidator< | ||
V extends ValidityState = ValidityState, | ||
S extends MultiStateValidatorStates = MultiStateValidatorStates | ||
>(states: S, validator: MultiStateValidator<V, S>, initialValidity: V = [undefined] as V): UseValidatorReturn<V> { | ||
if (typeof states !== 'object') { | ||
throw Error('states expected to be an object or array, got ' + typeof states); | ||
} | ||
|
||
const validatorFn = useRef(validator); | ||
|
||
const [validity, setValidity] = useState(initialValidity); | ||
|
||
const deps = Array.isArray(states) ? states : Object.values(states); | ||
const validate = useCallback(() => { | ||
if (validatorFn.current.length === 2) { | ||
validatorFn.current(states, setValidity); | ||
} else { | ||
setValidity(validatorFn.current(states)); | ||
} | ||
}, deps); | ||
|
||
useEffect(() => { | ||
validate(); | ||
}, deps); | ||
|
||
return [validity, validate]; | ||
} |