Skip to content
Permalink
Browse files

Improve handling of non event values in handleChange and handleBlur (#…

…1216)

This PR improves `handleChange` and `handleBlur` for cases where they are not passed an event, for usage with React Native (Web) and other UI libraries, where the library currently struggles. 

- `handleBlur` can now be called with undefined instead of an event.
- `handleChange` can now no only be called with a string value, but also any other value

The gist of this PR is checking for `isEvent` instead of checking for `!isString`. This makes the handling much more straight forward. I also took the liberty to refactor some of the code to be less dense (no confusing ternary assignment returns) and improve the types for `handleBlur`.
  • Loading branch information...
MrLoh authored and jaredpalmer committed Apr 2, 2019
1 parent 030c2af commit 899b9e91d1c0af9a0874f2b69edb955d2f24df05
Showing with 89 additions and 72 deletions.
  1. +12 βˆ’1 .all-contributorsrc
  2. +1 βˆ’3 README.md
  3. +70 βˆ’66 src/Formik.tsx
  4. +2 βˆ’2 src/types.tsx
  5. +4 βˆ’0 src/utils.ts
@@ -111,6 +111,17 @@
"bug",
"doc"
]
},
{
"login": "MrLoh",
"name": "Tobias Lohse",
"avatar_url": "https://avatars0.githubusercontent.com/u/1285032?v=4",
"profile": "http://MrLoh.se",
"contributions": [
"bug",
"code"
]
}
]
],
"repoType": "github"
}
@@ -68,12 +68,10 @@ Formik is made with <3 thanks to these wonderful people
([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->

<!-- prettier-ignore -->
| [<img src="https://avatars2.githubusercontent.com/u/4060187?v=4" width="100px;"/><br /><sub><b>Jared Palmer</b></sub>](http://jaredpalmer.com)<br />[πŸ’¬](#question-jaredpalmer "Answering Questions") [πŸ’»](https://github.com/jaredpalmer/formik/commits?author=jaredpalmer "Code") [🎨](#design-jaredpalmer "Design") [πŸ“–](https://github.com/jaredpalmer/formik/commits?author=jaredpalmer "Documentation") [πŸ’‘](#example-jaredpalmer "Examples") [πŸ€”](#ideas-jaredpalmer "Ideas, Planning, & Feedback") [πŸ‘€](#review-jaredpalmer "Reviewed Pull Requests") [⚠️](https://github.com/jaredpalmer/formik/commits?author=jaredpalmer "Tests") | [<img src="https://avatars0.githubusercontent.com/u/109324?v=4" width="100px;"/><br /><sub><b>Ian White</b></sub>](https://www.stardog.io)<br />[πŸ’¬](#question-eonwhite "Answering Questions") [πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Aeonwhite "Bug reports") [πŸ’»](https://github.com/jaredpalmer/formik/commits?author=eonwhite "Code") [πŸ“–](https://github.com/jaredpalmer/formik/commits?author=eonwhite "Documentation") [πŸ€”](#ideas-eonwhite "Ideas, Planning, & Feedback") [πŸ‘€](#review-eonwhite "Reviewed Pull Requests") | [<img src="https://avatars0.githubusercontent.com/u/829963?v=4" width="100px;"/><br /><sub><b>Andrej Badin</b></sub>](http://andrejbadin.com)<br />[πŸ’¬](#question-Andreyco "Answering Questions") [πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3AAndreyco "Bug reports") [πŸ“–](https://github.com/jaredpalmer/formik/commits?author=Andreyco "Documentation") | [<img src="https://avatars2.githubusercontent.com/u/91115?v=4" width="100px;"/><br /><sub><b>Adam Howard</b></sub>](http://adz.co.de)<br />[πŸ’¬](#question-skattyadz "Answering Questions") [πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Askattyadz "Bug reports") [πŸ€”](#ideas-skattyadz "Ideas, Planning, & Feedback") [πŸ‘€](#review-skattyadz "Reviewed Pull Requests") | [<img src="https://avatars1.githubusercontent.com/u/6711845?v=4" width="100px;"/><br /><sub><b>Vlad Shcherbin</b></sub>](https://github.com/VladShcherbin)<br />[πŸ’¬](#question-VladShcherbin "Answering Questions") [πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3AVladShcherbin "Bug reports") [πŸ€”](#ideas-VladShcherbin "Ideas, Planning, & Feedback") | [<img src="https://avatars3.githubusercontent.com/u/383212?v=4" width="100px;"/><br /><sub><b>Brikou CARRE</b></sub>](https://github.com/brikou)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Abrikou "Bug reports") [πŸ“–](https://github.com/jaredpalmer/formik/commits?author=brikou "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/5314713?v=4" width="100px;"/><br /><sub><b>Sam Kvale</b></sub>](http://skvale.github.io)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Askvale "Bug reports") [πŸ’»](https://github.com/jaredpalmer/formik/commits?author=skvale "Code") [⚠️](https://github.com/jaredpalmer/formik/commits?author=skvale "Tests") |
| :---: | :---: | :---: | :---: | :---: | :---: | :---: |
| [<img src="https://avatars0.githubusercontent.com/u/13765558?v=4" width="100px;"/><br /><sub><b>Jon Tansey</b></sub>](http://jon.tansey.info)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Ajontansey "Bug reports") [πŸ’»](https://github.com/jaredpalmer/formik/commits?author=jontansey "Code") | [<img src="https://avatars0.githubusercontent.com/u/6819171?v=4" width="100px;"/><br /><sub><b>Tyler Martinez</b></sub>](http://slightlytyler.com)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Aslightlytyler "Bug reports") [πŸ“–](https://github.com/jaredpalmer/formik/commits?author=slightlytyler "Documentation") |

| [<img src="https://avatars0.githubusercontent.com/u/13765558?v=4" width="100px;"/><br /><sub><b>Jon Tansey</b></sub>](http://jon.tansey.info)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Ajontansey "Bug reports") [πŸ’»](https://github.com/jaredpalmer/formik/commits?author=jontansey "Code") | [<img src="https://avatars0.githubusercontent.com/u/6819171?v=4" width="100px;"/><br /><sub><b>Tyler Martinez</b></sub>](http://slightlytyler.com)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3Aslightlytyler "Bug reports") [πŸ“–](https://github.com/jaredpalmer/formik/commits?author=slightlytyler "Documentation") | [<img src="https://avatars0.githubusercontent.com/u/1285032?v=4" width="100px;"/><br /><sub><b>Tobias Lohse</b></sub>](http://MrLoh.se)<br />[πŸ›](https://github.com/jaredpalmer/formik/issues?q=author%3AMrLoh "Bug reports") [πŸ’»](https://github.com/jaredpalmer/formik/commits?author=MrLoh "Code") |
<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the
@@ -19,6 +19,7 @@ import {
isNaN,
isPromise,
isString,
isInputEvent,
setIn,
setNestedObjectValues,
getActiveElement,
@@ -40,7 +41,7 @@ export class Formik<Values = FormikValues> extends React.Component<
initialValues: Values;
didMount: boolean;
hcCache: {
[key: string]: (e: string | React.ChangeEvent<any>) => void;
[key: string]: (e: unknown | React.ChangeEvent<any>) => void;
} = {};
hbCache: {
[key: string]: (e: any) => void;
@@ -301,37 +302,23 @@ export class Formik<Values = FormikValues> extends React.Component<

handleChange = (
eventOrPath: string | React.ChangeEvent<any>
): void | ((eventOrTextValue: string | React.ChangeEvent<any>) => void) => {
// @todo someone make this less disgusting.
//
// executeChange is the core of handleChange, we'll use it cache change
// handlers like Preact's linkState.
): void | ((eventOrValue: unknown | React.ChangeEvent<any>) => void) => {
// this function actually handles the change
const executeChange = (
eventOrTextValue: string | React.ChangeEvent<any>,
eventOrValue: unknown | React.ChangeEvent<any>,
maybePath?: string
) => {
// By default, assume that the first argument is a string. This allows us to use
// handleChange with React Native and React Native Web's onChangeText prop which
// provides just the value of the input.
// To allow using handleChange with React Native (Web) or other UI libraries, we
// allow for the first argument to be either a value or the standard change event.
let field = maybePath;
let val = eventOrTextValue;
let parsed;
// If the first argument is not a string though, it has to be a synthetic React Event (or a fake one),
// so we handle like we would a normal HTML change event.
if (!isString(eventOrTextValue)) {
// If we can, persist the event
// @see https://reactjs.org/docs/events.html#event-pooling
if ((eventOrTextValue as React.ChangeEvent<any>).persist) {
(eventOrTextValue as React.ChangeEvent<any>).persist();
let value: unknown;
if (isInputEvent(eventOrValue)) {
const event = eventOrValue as React.ChangeEvent<any>;
// If we can, persist the event, https://reactjs.org/docs/events.html#event-pooling
if (event.persist) {
event.persist();
}
const {
type,
name,
id,
value,
checked,
outerHTML,
} = (eventOrTextValue as React.ChangeEvent<any>).target;
const { type, name, id, checked, outerHTML } = event.target;
field = maybePath ? maybePath : name ? name : id;
if (!field && process.env.NODE_ENV !== 'production') {
warnAboutMissingIdentifier({
@@ -340,42 +327,47 @@ export class Formik<Values = FormikValues> extends React.Component<
handlerName: 'handleChange',
});
}
val = /number|range/.test(type)
? ((parsed = parseFloat(value)), isNaN(parsed) ? '' : parsed)
: /checkbox/.test(type) ? checked : value;
value = event.target.value;
if (/number|range/.test(type)) {
const parsed = parseFloat(event.target.value);
value = isNaN(parsed) ? '' : parsed;
}
if (/checkbox/.test(type)) {
value = checked;
}
} else {
value = eventOrValue;
}

if (field) {
// Set form fields by name
this.setState(
prevState => ({
...prevState,
values: setIn(prevState.values, field!, val),
values: setIn(prevState.values, field!, value),
}),
() => {
if (this.props.validateOnChange) {
this.runValidations(setIn(this.state.values, field!, val));
this.runValidations(setIn(this.state.values, field!, value));
}
}
);
}
};

// Actually execute logic above....
// cache these handlers by key like Preact's linkState does for perf boost
if (isString(eventOrPath)) {
return isFunction(this.hcCache[eventOrPath])
? this.hcCache[eventOrPath] // return the cached handled
: (this.hcCache[eventOrPath] = (
// make a new one
event: React.ChangeEvent<any> | string
) =>
executeChange(
event /* string or event, does not matter */,
eventOrPath /* this is path to the field now */
));
const path = eventOrPath;
// cache these handlers by key like Preact's linkState does for perf boost
if (!isFunction(this.hcCache[path])) {
// set a new handle function in cache
this.hcCache[path] = (eventOrValue: unknown | React.ChangeEvent<any>) =>
executeChange(eventOrValue, path);
}
return this.hcCache[path]; // return the cached function
} else {
executeChange(eventOrPath);
const event = eventOrPath;
executeChange(event);
}
};

@@ -462,39 +454,51 @@ export class Formik<Values = FormikValues> extends React.Component<
this.props.onSubmit(this.state.values, this.getFormikActions());
};

handleBlur = (eventOrString: any): void | ((e: any) => void) => {
const executeBlur = (e: any, path?: string) => {
if (e.persist) {
e.persist();
}
const { name, id, outerHTML } = e.target;
const field = path ? path : name ? name : id;

if (!field && process.env.NODE_ENV !== 'production') {
warnAboutMissingIdentifier({
htmlContent: outerHTML,
documentationAnchorLink: 'handleblur-e-any--void',
handlerName: 'handleBlur',
});
handleBlur = (
eventOrPath: string | React.FocusEvent<any>
): void | ((e?: React.FocusEvent<any>) => void) => {
const executeBlur = (
maybeEvent?: React.FocusEvent<any>,
maybePath?: string
) => {
let field = maybePath;
if (isInputEvent(maybeEvent)) {
const event = maybeEvent as React.FocusEvent<any>;
// If we can, persist the event, https://reactjs.org/docs/events.html#event-pooling
if (event.persist) {
event.persist();
}
const { name, id, outerHTML } = event.target;
field = name ? name : id;
if (!field && process.env.NODE_ENV !== 'production') {
warnAboutMissingIdentifier({
htmlContent: outerHTML,
documentationAnchorLink: 'handleblur-e-reactfocuseventany--void',
handlerName: 'handleBlur',
});
}
}

this.setState(prevState => ({
touched: setIn(prevState.touched, field, true),
touched: setIn(prevState.touched, field!, true),
}));

if (this.props.validateOnBlur) {
this.runValidations(this.state.values);
}
};

if (isString(eventOrString)) {
if (isString(eventOrPath)) {
const path = eventOrPath;
// cache these handlers by key like Preact's linkState does for perf boost
return isFunction(this.hbCache[eventOrString])
? this.hbCache[eventOrString]
: (this.hbCache[eventOrString] = (event: any) =>
executeBlur(event, eventOrString));
if (!isFunction(this.hbCache[path])) {
// set a new handle function in cache
this.hbCache[path] = (event?: React.FocusEvent<any>) =>
executeBlur(event, path);
}
return this.hbCache[path]; // return the cached function
} else {
executeBlur(eventOrString);
const event = eventOrPath;
executeBlur(event);
}
};

@@ -145,11 +145,11 @@ export interface FormikHandlers {
/** Classic React change handler, keyed by input name */
handleChange(e: React.ChangeEvent<any>): void;
/** Preact-like linkState. Will return a handleChange function. */
handleChange<T = string | React.ChangeEvent<any>>(
handleChange<T = unknown | React.ChangeEvent<any>>(
field: T
): T extends React.ChangeEvent<any>
? void
: ((e: string | React.ChangeEvent<any>) => void);
: ((e: unknown | React.ChangeEvent<any>) => void);
}

/**
@@ -126,6 +126,10 @@ export const isEmptyChildren = (children: any): boolean =>
export const isPromise = (value: any): value is PromiseLike<any> =>
isObject(value) && isFunction(value.then);

/** @private is the given object/value a type of synthetic event? */
export const isInputEvent = (value: any): value is React.SyntheticEvent<any> =>
value && isObject(value) && isObject(value.target);

/**
* Same as document.activeElement but wraps in a try-catch block. In IE it is
* not safe to call document.activeElement if there is nothing focused.

0 comments on commit 899b9e9

Please sign in to comment.
You can’t perform that action at this time.