Skip to content
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
16 changes: 16 additions & 0 deletions app/pages/tutorial/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Basic = () => (
email: '',
favorite: '',
checked: [],
checkedNum: [],
picked: '',
}}
validationSchema={Yup.object().shape({
Expand Down Expand Up @@ -64,6 +65,21 @@ const Basic = () => (
Three
</label>
</div>
<div id="checkbox-group">Numeric Checkbox Group </div>
<div role="group" aria-labelledby="checkbox-group">
<label>
<Field type="checkbox" name="checkedNum" value={1} parse={value => parseInt(value, 10)} />
One
</label>
<label>
<Field type="checkbox" name="checkedNum" value={2} parse={value => parseInt(value, 10)} />
Two
</label>
<label>
<Field type="checkbox" name="checkedNum" value={3} parse={value => parseInt(value, 10)} />
Three
</label>
</div>
<div id="my-radio-group">Picked</div>
<div role="group" aria-labelledby="my-radio-group">
<label>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"scripts": {
"lerna": "lerna",
"dev": "lerna run start --stream --parallel",
"test": "lerna run test --",
"test": "lerna run test -- --silent",
"test:watch": "lerna run test --stream --no-sort -- --watch",
"test:watchAll": "lerna run test --stream --no-sort -- --watchAll",
"test:vscode": "lerna run test --stream --no-sort --",
Expand Down
74 changes: 19 additions & 55 deletions packages/formik/src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
HandleChangeEventFn,
HandleChangeFn,
FormikSharedConfig,
InputElements,
} from './types';
import {
isFunction,
Expand All @@ -26,7 +27,6 @@ import {
getActiveElement,
getIn,
setNestedObjectValues,
isReactNative,
} from './utils';
import { FormikProvider } from './FormikContext';
import invariant from 'tiny-warning';
Expand All @@ -36,6 +36,7 @@ import {
} from './helpers/form-helpers';
import { useFormikSubscriptions } from './hooks/useFormikSubscriptions';
import { useEventCallback } from './hooks/useEventCallback';
import { selectFieldOnChange } from './helpers/field-helpers';

export type FormikMessage<Values> =
| { type: 'SUBMIT_ATTEMPT' }
Expand Down Expand Up @@ -697,59 +698,50 @@ export function useFormik<Values extends FormikValues = FormikValues>(
);

const executeChange = React.useCallback(
(eventOrTextValue: string | React.ChangeEvent<any>, maybePath?: string) => {
(eventOrTextValue: string | React.ChangeEvent<React.ElementRef<InputElements>>, 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.
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 any).persist) {
(eventOrTextValue as React.ChangeEvent<any>).persist();
//
// but first, check if someone might have faked this value
if (typeof eventOrTextValue.persist !== "undefined") {
eventOrTextValue.persist();
}

const target = eventOrTextValue.target
? (eventOrTextValue as React.ChangeEvent<any>).target
: (eventOrTextValue as React.ChangeEvent<any>).currentTarget;
? eventOrTextValue.target
: eventOrTextValue.currentTarget;

const {
type,
name,
id,
value,
checked,
outerHTML,
options,
multiple,
} = target;

field = maybePath ? maybePath : name ? name : id;

if (!field && __DEV__) {
warnAboutMissingIdentifier({
htmlContent: outerHTML,
documentationAnchorLink: 'handlechange-e-reactchangeeventany--void',
handlerName: 'handleChange',
});
}
val = /number|range/.test(type)
? ((parsed = parseFloat(value)), isNaN(parsed) ? '' : parsed)
: /checkbox/.test(type) // checkboxes
? getValueForCheckbox(getIn(getState().values, field!), checked, value)
: options && multiple // <select multiple>
? getSelectedValues(options)
: value;
}

if (field) {
// Set form fields by name
setFieldValue(field, val);
selectFieldOnChange({ setFieldValue, getState })(eventOrTextValue);

} else if (field) {
selectFieldOnChange({ setFieldValue, getState }, field)(eventOrTextValue);
}
},
[setFieldValue, getState().values]
[setFieldValue]
);

const handleChange = useEventCallback(
Expand Down Expand Up @@ -984,33 +976,6 @@ export function useFormik<Values extends FormikValues = FormikValues>(
resetForm();
});

const getValueFromEvent = useEventCallback(
(event: React.SyntheticEvent<any>, fieldName: string) => {
// React Native/Expo Web/maybe other render envs
if (
!isReactNative &&
event.nativeEvent &&
(event.nativeEvent as any).text !== undefined
) {
return (event.nativeEvent as any).text;
}

// React Native
if (isReactNative && event.nativeEvent) {
return (event.nativeEvent as any).text;
}

const target = event.target ? event.target : event.currentTarget;
const { type, value, checked, options, multiple } = target;

return /checkbox/.test(type) // checkboxes
? getValueForCheckbox(getIn(getState().values, fieldName!), checked, value)
: !!multiple // <select multiple>
? getSelectedValues(options)
: value;
}
);

// mostly optimized renders
return {
// config
Expand Down Expand Up @@ -1042,7 +1007,6 @@ export function useFormik<Values extends FormikValues = FormikValues>(
validateField,
unregisterField,
registerField,
getValueFromEvent,
// state helpers
getState,
useState,
Expand Down Expand Up @@ -1203,7 +1167,7 @@ function arrayMerge(target: any[], source: any[], options: any): any[] {
}

/** Return multi select values based on an array of options */
function getSelectedValues(options: any[]) {
export function getSelectedValues(options: HTMLOptionsCollection) {
const result = [];
if (options) {
for (let index = 0; index < options.length; index++) {
Expand All @@ -1217,7 +1181,7 @@ function getSelectedValues(options: any[]) {
}

/** Return the next value for a checkbox */
function getValueForCheckbox(
export function getValueForCheckbox(
currentValue: string | any[],
checked: boolean,
valueProp: any
Expand Down
2 changes: 0 additions & 2 deletions packages/formik/src/FormikContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export const FormikProvider = <Values,>(props: React.PropsWithChildren<FormikPro
validateField,
unregisterField,
registerField,
getValueFromEvent,
// state helpers
getState,
useState,
Expand Down Expand Up @@ -82,7 +81,6 @@ export const FormikProvider = <Values,>(props: React.PropsWithChildren<FormikPro
validateField,
unregisterField,
registerField,
getValueFromEvent,
getState,
useState,
}),
Expand Down
69 changes: 64 additions & 5 deletions packages/formik/src/helpers/field-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { FormikReducerState } from "../types";
import { getIn } from "../utils";
import { FieldHookConfig } from '../Field';
import { getSelectedValues, getValueForCheckbox } from '../Formik';
import { FormikApi, FormikReducerState, InputElements } from '../types';
import { getIn, getValueFromEvent, isInputEvent, isObject } from '../utils';

/**
* @internal
*
* Get FieldMetaProps from state.
*/
export const selectFieldMetaByName = <
Values
>(name: string) => (
export const selectFieldMetaByName = <Values>(name: string) => (
state: Pick<
FormikReducerState<Values>,
| 'values'
Expand Down Expand Up @@ -37,3 +37,62 @@ export const numberParseFn = (value: any, _name: string) => {

export const defaultFormatFn = (value: unknown, _name: string) =>
value === undefined ? '' : value;

export const selectFieldOnChange = <Value, Values>(
{
setFieldValue,
getState,
}: Pick<FormikApi<Values>, 'setFieldValue' | 'getState'>,
nameOrConfig?: string | FieldHookConfig<Value>
) => {
return (
eventOrValue: React.ChangeEvent<React.ElementRef<InputElements>> | unknown
) => {
const {
type = '',
name = '',
parse: configParse,
multiple,
}: FieldHookConfig<Value> = isObject(nameOrConfig)
? nameOrConfig
: isInputEvent(eventOrValue) && eventOrValue.target
? (eventOrValue.target as any)
: { name: nameOrConfig! };

if (isInputEvent(eventOrValue)) {
if (eventOrValue.persist) {
eventOrValue.persist();
}
}

const value = isInputEvent(eventOrValue)
? getValueFromEvent(eventOrValue)
: eventOrValue;

const defaultParse = /number|range/.test(type)
? numberParseFn
: // radios and checkboxes don't have a default parser
// the default parser won't work for grouped fields
!/radio|checkbox/.test(type)
? defaultParseFn
: undefined;

const parse = configParse ?? defaultParse;
const parsedValue = parse ? parse(value, name) : value;

setFieldValue(
name,
/checkbox/.test(type) &&
isInputEvent(eventOrValue) &&
eventOrValue.target instanceof HTMLInputElement
? getValueForCheckbox(
getIn(getState().values, name!),
eventOrValue.target.checked,
parsedValue
)
: multiple && isInputEvent(eventOrValue) && eventOrValue.target instanceof HTMLSelectElement // <select multiple>
? getSelectedValues(eventOrValue.target.options)
: parsedValue
);
};
};
Loading