Skip to content

Commit

Permalink
Merge 4b4b594 into 761ac91
Browse files Browse the repository at this point in the history
  • Loading branch information
wsmd committed Jan 5, 2020
2 parents 761ac91 + 4b4b594 commit ddcf2bd
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 65 deletions.
10 changes: 9 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,13 @@
"plugins": ["@babel/plugin-transform-runtime"]
}
},
"presets": [["@babel/preset-env", { "modules": false }]]
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"exclude": ["transform-typeof-symbol"]
}
]
]
}
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"extends": [
"@wsmd/eslint-config/typescript",
"@wsmd/eslint-config/react",
"@wsmd/eslint-config/prettier",
"@wsmd/eslint-config/jest"
],
"rules": {
"getter-return": "off",
"consistent-return": "off"
"consistent-return": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-console": ["error", { "allow": ["warn", "error"] }]
},
"overrides": [
{
Expand Down
41 changes: 29 additions & 12 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,18 @@ interface FormState<T, E = StateErrors<T, string>> {
reset(): void;
clear(): void;
setField<K extends keyof T>(name: K, value: T[K]): void;
setFieldError(name: keyof T, error: string): void;
setField<K extends keyof T>(field: {
name: K;
value?: T[K];
error?: any;
validity?: boolean;
touched?: boolean;
pristine?: boolean;
}): void;
setFieldError(name: keyof T, error: any): void;
clearField(name: keyof T): void;
resetField(name: keyof T): void;
isPristine(): boolean;
}

interface FormOptions<T> {
Expand Down Expand Up @@ -75,7 +84,7 @@ interface Inputs<T, Name extends keyof T = keyof T> {
number: InputInitializer<T, Args<Name>, BaseInputProps<T>>;
range: InputInitializer<T, Args<Name>, BaseInputProps<T>>;
tel: InputInitializer<T, Args<Name>, BaseInputProps<T>>;
radio: InputInitializer<T, Args<Name, OwnValue>, RadioProps<T>>;
radio: InputInitializer<T, Args<Name, OwnValue>, CheckboxProps<T>>;
date: InputInitializer<T, Args<Name>, BaseInputProps<T>>;
month: InputInitializer<T, Args<Name>, BaseInputProps<T>>;
week: InputInitializer<T, Args<Name>, BaseInputProps<T>>;
Expand All @@ -88,7 +97,9 @@ interface Inputs<T, Name extends keyof T = keyof T> {
* in the form state will be of type boolean
*/
checkbox(name: Name, ownValue?: OwnValue): CheckboxProps<T>;
checkbox(options: InputOptions<T, Name, Maybe<OwnValue>>): CheckboxProps<T>;
checkbox<Name extends keyof T>(
options: InputOptions<T, Name, Maybe<OwnValue>>,
): CheckboxProps<T>;

raw<RawValue, Name extends keyof T = keyof T>(
name: Name,
Expand All @@ -101,12 +112,12 @@ interface Inputs<T, Name extends keyof T = keyof T> {
id(name: string, value?: string): string;
}

interface InputInitializer<T, Args extends any[], ReturnValue> {
(...args: Args): ReturnValue;
(options: InputOptions<T, Args[0], Args[1]>): ReturnValue;
interface InputInitializer<T, Args extends any[], InputProps> {
(...args: Args): InputProps;
(options: InputOptions<T, Args[0], Args[1]>): InputProps;
}

type InputOptions<T, Name, Value = void> = {
type InputOptions<T, Name extends keyof T, Value> = {
name: Name;
validateOnBlur?: boolean;
touchOnChange?: boolean;
Expand All @@ -115,13 +126,21 @@ type InputOptions<T, Name, Value = void> = {
values: StateValues<T>,
event: React.ChangeEvent<InputElement> | React.FocusEvent<InputElement>,
): any;
compare?(
initialValue: StateValues<T>[Name],
value: StateValues<T>[Name],
): boolean;
onChange?(event: React.ChangeEvent<InputElement>): void;
onBlur?(event: React.FocusEvent<InputElement>): void;
} & WithValue<Value>;

interface RawInputOptions<T, Name extends keyof T, RawValue> {
name: Name;
validateOnBlur?: boolean;
compare?(
initialValue: StateValues<T>[Name],
value: StateValues<T>[Name],
): boolean;
touchOnChange?: boolean;
validate?(
value: StateValues<T>[Name],
Expand All @@ -139,10 +158,10 @@ interface RawInputProps<T, Name extends keyof T, RawValue> {
onBlur(...args: any[]): any;
}

type WithValue<V> = V extends OwnValue
? { value: OwnValue }
: V extends undefined
type WithValue<V extends any> = V extends Maybe<OwnValue>
? { value?: OwnValue }
: V extends OwnValue
? { value: OwnValue }
: {};

type Args<Name, Value = void> = [Name, Value];
Expand All @@ -164,8 +183,6 @@ interface CheckboxProps<T> extends BaseInputProps<T> {
checked: boolean;
}

interface RadioProps<T> extends CheckboxProps<T> {}

interface MultipleProp {
multiple: boolean;
}
Expand Down
7 changes: 5 additions & 2 deletions src/parseInputArgs.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { identity, noop, isEqualByValue } from './utils';
import { identity, noop, toString } from './utils';

const defaultInputOptions = {
onChange: identity,
onBlur: noop,
validate: null,
validateOnBlur: undefined,
touchOnChange: false,
compare: isEqualByValue,
compare: null,
};

export function parseInputArgs(args) {
Expand All @@ -19,9 +19,12 @@ export function parseInputArgs(args) {
[{ name, value: ownValue, ...options }] = args;
}

ownValue = toString(ownValue);

return {
name,
ownValue,
hasOwnValue: !!ownValue,
...defaultInputOptions,
...options,
};
Expand Down
63 changes: 41 additions & 22 deletions src/useFormState.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef } from 'react';
import { toString, noop, omit, isFunction, isEmpty } from './utils';
import { noop, omit, isFunction, isEmpty, isEqual } from './utils';
import { parseInputArgs } from './parseInputArgs';
import { useInputId } from './useInputId';
import { useCache } from './useCache';
Expand Down Expand Up @@ -39,32 +39,32 @@ export default function useFormState(initialState, options) {
function warn(key, type, message) {
if (!devWarnings.has(`${type}:${key}`)) {
devWarnings.set(`${type}:${key}`, true);
// eslint-disable-next-line no-console
console.warn(CONSOLE_TAG, message);
}
}

const createPropsGetter = type => (...args) => {
const { name, ownValue, ...inputOptions } = parseInputArgs(args);
const { name, ownValue, hasOwnValue, ...inputOptions } = parseInputArgs(
args,
);

const hasOwnValue = !!toString(ownValue);
const isCheckbox = type === CHECKBOX;
const isRadio = type === RADIO;
const isSelectMultiple = type === SELECT_MULTIPLE;
const isRaw = type === RAW;
const hasValueInState = formState.current.values[name] !== undefined;

// This is used to cache input props that shouldn't change across
// re-renders. Note that for `raw` values, `toString(ownValue)`
// re-renders. Note that for `raw` values, `ownValue`
// will return '[object Object]'. This means we can't have multiple
// raw inputs with the same name and different values, but this is
// probably fine.
const key = `${type}.${name}.${toString(ownValue)}`;
const key = `${type}.${name}.${ownValue}`;

function setDefaultValue() {
/* istanbul ignore else */
if (process.env.NODE_ENV === 'development') {
if (isRaw && formState.current.values[name] === undefined) {
if (isRaw && !hasValueInState) {
warn(
key,
'missingInitialValue',
Expand Down Expand Up @@ -112,6 +112,28 @@ export default function useFormState(initialState, options) {
);
}

function getCompareFn() {
if (isFunction(inputOptions.compare)) {
return inputOptions.compare;
}
if (isRaw) {
/* istanbul ignore else */
if (process.env.NODE_ENV === 'development') {
warn(
key,
'missingCompare',
`You used a raw input type for "${name}" without providing a ` +
'custom compare method. As a result, the pristine value of ' +
'this input will remain set to "false" after a change. If the ' +
'form depends on the pristine values, please provide a custom ' +
'compare method.',
);
}
return () => false;
}
return isEqual;
}

function validate(
e,
value = isRaw ? formState.current.values[name] : e.target.value,
Expand Down Expand Up @@ -165,7 +187,7 @@ export default function useFormState(initialState, options) {
get checked() {
const { values } = formState.current;
if (isRadio) {
return values[name] === toString(ownValue);
return values[name] === ownValue;
}
if (isCheckbox) {
if (!hasOwnValue) {
Expand All @@ -177,9 +199,7 @@ export default function useFormState(initialState, options) {
* <input {...input.checkbox('option1')} />
* <input {...input.checkbox('option1', 'value_of_option1')} />
*/
return hasValueInState
? values[name].includes(toString(ownValue))
: false;
return hasValueInState ? values[name].includes(ownValue) : false;
}
},
get value() {
Expand All @@ -196,13 +216,18 @@ export default function useFormState(initialState, options) {
formState.setTouched({ [name]: false });
}

// auto populating default values of pristine
if (formState.current.pristine[name] == null) {
formState.setPristine({ [name]: true });
}

/**
* Since checkbox and radio inputs have their own user-defined values,
* and since checkbox inputs can be either an array or a boolean,
* returning the value of input from the current form state is illogical
*/
if (isCheckbox || isRadio) {
return toString(ownValue);
return ownValue;
}

return hasValueInState ? formState.current.values[name] : '';
Expand Down Expand Up @@ -247,13 +272,6 @@ export default function useFormState(initialState, options) {
touch(e);
}

const isPristine = inputOptions.compare(
formState.initialValues.get(name),
value,
);

formState.setPristine(isPristine ? omit(name) : { [name]: false });

const partialNewState = { [name]: value };
const newValues = { ...formState.current.values, ...partialNewState };

Expand All @@ -267,6 +285,8 @@ export default function useFormState(initialState, options) {
validate(e, value, newValues);
}

formState.updatePristine(name, value, getCompareFn());

formState.setValues(partialNewState);
}),
onBlur: callbacks.getOrSet(ON_CHANGE_HANDLER + key, e => {
Expand Down Expand Up @@ -299,11 +319,10 @@ export default function useFormState(initialState, options) {
};

const formStateAPI = useRef({
isPristine: formState.isPristine,
clearField: formState.clearField,
resetField: formState.resetField,
setField(name, value) {
formState.setField(name, value, true, true);
},
setField: formState.setField,
setFieldError(name, error) {
formState.setValidity({ [name]: false });
formState.setError({ [name]: error });
Expand Down
40 changes: 29 additions & 11 deletions src/useState.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useReducer, useRef } from 'react';
import { isFunction, omit, isEqualByValue } from './utils';
import { isFunction, getOr } from './utils';
import { useCache } from './useCache';

function stateReducer(state, newState) {
Expand All @@ -17,24 +17,40 @@ export function useState({ initialState }) {

state.current = { values, touched, validity, errors, pristine };

function setField(name, value, inputValidity, inputTouched, inputError) {
setValues({ [name]: value });
setTouched({ [name]: inputTouched });
setValidity({ [name]: inputValidity });
setError({ [name]: inputError });
function getInitialValue(name) {
return initialValues.has(name)
? initialValues.get(name)
: getOr(initialState, state.current.values)[name];
}

function updatePristine(name, value, comparator) {
const initialValue = getInitialValue(name);
setPristine({ [name]: !!comparator(initialValue, value) });
}

const isPristine = isEqualByValue(initialValues.get(name), value);
setPristine(isPristine ? omit(name) : { [name]: false });
function setFieldState(field) {
setError({ [field.name]: field.error });
setValues({ [field.name]: field.value });
setTouched({ [field.name]: getOr(field.touched, true) });
setValidity({ [field.name]: getOr(field.validity, true) });
setPristine({ [field.name]: getOr(field.pristine, true) });
}

function setField(name, value) {
setFieldState(typeof name === 'object' ? name : { name, value });
}

function clearField(name) {
setField(name);
}

function resetField(name) {
setField(
name,
initialValues.has(name) ? initialValues.get(name) : initialState[name],
setField(name, getInitialValue(name));
}

function isPristine() {
return Object.keys(state.current.pristine).every(
key => !!state.current.pristine[key],
);
}

Expand All @@ -52,9 +68,11 @@ export function useState({ initialState }) {
setError,
setField,
setPristine,
updatePristine,
initialValues,
resetField,
clearField,
forEach,
isPristine,
};
}

0 comments on commit ddcf2bd

Please sign in to comment.