Formik field values arn't being passed from React Context - javascript

I have a Formik form that is using a progressive stepper and have multiple fields across different components, thus requiring the need to store the values in React Context. However none of the field values are being passed, so when I click submit, all values are empty strings and the validation fails. You can see on each Formik Field i am setting the value as {state.[field]}, which comes from the Context, so I believe something is going wrong here. Can anyone see what I'm doing wrong?
Thanks a lot
Here is my parent component
const AddSongPage = () => {
const { state, dispatch } = useUploadFormContext();
const initialValues = {
name: "",
};
const { mutate: handleCreateTrack } = useCreateSyncTrackMutation(
gqlClient,
{}
);
const handleSubmit = (values: any) => {
handleCreateTrack(
{
...values,
},
{
onSuccess() {
console.log("Track added succesfully");
},
}
);
};
const validate = Yup.object({
name: Yup.string().required("Song name is required"),
description: Yup.string().optional(),
});
return (
<Layout headerBg="brand.blue">
<Formik
onSubmit={(values) => handleSubmit(values)}
initialValues={initialValues}
validationSchema={validate}
>
<Form>
<Box> {state.activeStep === 1 && <Step1 />}</Box>
<Box> {state.activeStep === 2 && <Step2 />}</Box>
<Box> {state.activeStep === 3 && <Step3 />}</Box>
<Button type={"submit"}>Submit</Button>
</Form>
</Formik>
</Layout>
);
};
Here is step 1
const Step1 = () => {
const { state, dispatch } = useUploadFormContext();
const onInputChange = (e: FormEvent<HTMLInputElement>) => {
const inputName = e.currentTarget.name;
dispatch({
type: "SET_UPLOAD_FORM",
payload: {
[inputName]: e.currentTarget.value,
},
});
};
return (
<Stack spacing={4}>
<Field name={"name"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.name}
/>
</FormControl>
)}
</Field>
<Field name={"description"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.description}
/>
</FormControl>
)}
</Field>
</Stack>
);
};
export default Step1;

Related

Formik - Render ErrorMessage automatically

I have the following code which you can find here:
https://stackblitz.com/edit/react-d2fadr?file=src%2FApp.js
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
<ErrorMessage name={fieldName} />
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
I want to know if is it any way to render the ErrorMessage element automatically?
The error message should be shown somewhere around the input text.
If you know how, you can fork the StackBlitz above with your suggestion.
Thanks!
Don't really know why ErroMessage is not rendering before you submit your form once but you can replace the line <ErrorMessage name={fieldName} /> by {formik.errors[fieldName]} to make it works
import { ErrorMessage, Field, Form, Formik } from 'formik';
import React from 'react';
import { Button } from 'react-bootstrap';
import * as Yup from 'yup';
let fieldName = 'hexColor';
const TextInput = ({ field, value, placeholder, handleChange }) => {
value = (field && field.value) || value || '';
placeholder = placeholder || '';
return (
<input
type="text"
placeholder={placeholder}
onChange={(e) => handleChange(e.target.value)}
value={value}
/>
);
};
export default () => {
const onSubmit = (values, { setSubmitting }) => {
console.log(values);
setSubmitting(false);
};
return (
<Formik
initialValues={{ [fieldName]: 'ff0000' }}
validationSchema={Yup.object({
hexColor: Yup.string().test(
fieldName,
'The Hex Color is Wrong.',
(value) => {
return /^[0-9a-f]{6}$/.test(value);
}
),
})}
onSubmit={onSubmit}
enableReinitialize
>
{(formik) => {
const handleChange = (value) => {
value = value.replace(/[^0-9a-f]/g, '');
formik.setFieldValue(fieldName, value);
};
return (
<Form>
<div>
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
handleChange={handleChange}
/>
{formik.errors[fieldName]}
</div>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
>
Submit
</Button>
</Form>
);
}}
</Formik>
);
};
the issue is validation schema. When I changed 6
return /^[0-9a-f]{6}$/.test(value);
to 3
return /^[0-9a-f]{3}$/.test(value);
and submitted with the initial value, ErrorMessage component is rendered
To reach your goal, I changed your code as below:
Since Formik's default component is Input, I deleted your TextInput component as there was nothing special in your component and handleChange function.
<Field name="hexColor" placeholder="Hex Color" onChange={(e) => handleChange(e.target.value)}/>
<ErrorMessage name="hexColor" />
As in mentioned in this answer, I changed your submit button condition to determine whether the button is disabled or not:
<Button type="submit" disabled={Object.keys(errors).length}>
Submit
</Button>
You can view my entire solution here.
Edit
If you want to keep your component, you should pass props as you might be missing something important, e.g. onChange,onBlur etc.
const TextInput = ({ field, ...props }) => {
return (
<input
{...field} {...props}
// ... your custom things
/>
);
};
<Field
component={TextInput}
name={fieldName}
placeholder="Hex Color"
onChange={(e) => handleChange(e.target.value)}
/>
Solution 2

Moving from v4 to v5 broke my CleaveJS controlled TextField implementation, and I can't figure out how to fix it

Fiddle: https://codesandbox.io/s/priceless-bohr-x04cjf?file=/src/App.js
const CleaveField:FC<FormFieldProps> = (props) => {
const {onBlur = null, display, inputProps, InputProps, control, name, register, initialValue, errors, info} = props
return (
<Box errors={errors} info={info}>
<Controller
name={name}
control={control}
defaultValue={initialValue ? initialValue : ""}
onBlur={onBlur}
render={({ field }) => {
if (onBlur) {
field = {...field, onBlur}
}
return (
<TextField
{...field}
{...FORM_DEFAULTS}
error={errors && errors.length > 0}
label={display}
inputProps={inputProps}
InputProps={{
inputComponent: CleaveTextField
}}
/>
)
}}
/>
</Box>
)
}
export const CleaveTextField:FC = ({ inputRef, ...otherProps }) => (
<Cleave {...otherProps} inputRef={(ref) => inputRef(ref)} />
)
Above is the code that was stable in v4. The problem is that the TextField improperly initializes the initialValue. It does not trigger the TextField value != zero mechanism, pictured below:
There are some instructions for the migration that I attempted, but so far my attempts have been fruitless. https://mui.com/guides/migration-v4/#heading-textfield
//Change ref forwarding expectations on custom inputComponent. The component should forward the ref prop instead of the inputRef prop.
-function NumberFormatCustom(props) {
- const { inputRef, onChange, ...other } = props;
+const NumberFormatCustom = React.forwardRef(function NumberFormatCustom(
+ props,
+ ref,
+) {
const { onChange, ...other } = props;
return (
<NumberFormat
{...other}
- getInputRef={inputRef}
+ getInputRef={ref}
I have reviewed the git issues and there is nothing outstanding, so either my use case is narrow and there is a bug or I am failing to implement the new API for v5. In either case I have spent way more time than I'd like to admit trying every combination of props to fix this, to no avail.
If I cannot find a solution, my workaround will be to enable the helperText field instead of label, which will break consistency for my form designs.
If anyone has insight here I would be hugely grateful! Thanks.
Current iteration: (Fails identically to the legacy code)
const CleaveField:FC<FormFieldProps> = (props) => {
const {onBlur = null, setValue: setValueForm, display, inputProps, InputProps, control, name, register, initialValue = "", errors, info} = props
const {
field: { onChange, onBlur:onBlurField, name:formName, value, ref },
fieldState: { invalid, isTouched, isDirty },
formState: { touchedFields, dirtyFields }
} = useController({
name,
control,
defaultValue: initialValue,
});
return (
<Box errors={errors} info={info}>
<TextField
{...FORM_DEFAULTS}
value={value}
name={formName}
error={errors && errors.length > 0}
label={display}
onChange={onChange}
onBlur={onBlur || onBlurField}
inputProps={{...inputProps}}
InputProps={{
inputComponent: CleaveTextField,
}}
/>
</Box>
);
}
export const CleaveTextField = React.forwardRef(function CleaveTextField(
props,
ref,
) {
return <Cleave {...props} ref={ref}/>
})

Why does my Form Data Value does not correspond to submitted Input Value?

I'm currently trying to create a dynamic select/input component where you can choose values from select options or
type your own value inside an input field by selecting the "other" select option.
Right now I get stuck by updating the form data equally to the value of the selected option / input value. The Form Data Value always persist on the initial / default value.
App.js
...
export default function App() {
const methods = useForm({});
const { handleSubmit } = methods;
const customSalutationOptions = [
{ title: "Not specified", value: "null" },
{ title: "Male", value: "male" },
{ title: "Female", value: "female" }
];
const defaultValues = {
salutation: "null"
};
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="App">
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<SelectOrInput
variant="outlined"
name={`contactPerson[0].salutation`}
defaultValue={defaultValues}
selectOptions={customSalutationOptions}
/>
<Button type="submit" color="primary" fullWidth variant="contained">
Submit
</Button>
</form>
</FormProvider>
</div>
);
}
components/SelectOrInput.tsx
...
type Props = {
name: string;
label: string;
selectOptions: [{ title: string; value: string }];
defaultValue: any;
shouldUnregister: boolean;
variant: "filled" | "outlined" | "standard";
};
export default function SelectOrInput({
name,
label,
selectOptions,
defaultValue,
shouldUnregister,
variant
}: Props) {
const classes = useStyles();
const { control } = useFormContext();
const [showCustomInput, setShowCustomInput] = useState(false);
const [value, setValue] = useState(selectOptions[0].value);
const additionalInput = [{ title: "Other", value: "" }];
const combindedOptions = selectOptions.concat(additionalInput);
const handleInputSelectChange = (
event: React.ChangeEvent<{ value: unknown }>
): void => {
const value = event.target.value as string;
if (value === "") {
const newState = !showCustomInput;
setShowCustomInput(newState);
console.log(value);
setValue(value);
} else {
setValue(value);
}
};
const resetCustomInputToSelect = (event: React.MouseEvent<HTMLElement>) => {
const newState = !showCustomInput;
setValue(combindedOptions[0].value);
setShowCustomInput(newState);
};
return (
<>
{showCustomInput ? (
<FormControl className={classes.input}>
<Controller
name={name}
control={control}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<TextField
{...field}
label={label}
InputLabelProps={{ shrink: true }}
variant={variant}
placeholder="Other..."
autoFocus
type="text"
onChange={handleInputSelectChange}
value={value}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={resetCustomInputToSelect}
id="custominput-closebutton"
>
<CloseIcon fontSize="small" />
</IconButton>
</InputAdornment>
)
}}
></TextField>
)}
/>
</FormControl>
) : (
<FormControl className={classes.input} variant={variant}>
<InputLabel id={`label-select-${label}`}>{label}</InputLabel>
<Controller
name={name}
defaultValue={defaultValue}
control={control}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<Select
{...field}
label={label}
labelId={`label-select-${label}`}
value={value}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left"
},
getContentAnchorEl: null
}}
onChange={handleInputSelectChange}
>
{combindedOptions.map((option, index) => (
<MenuItem key={option.title} value={`${option.value}`}>
{option.title}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
)}
</>
);
}
...
To give a better example I provided a CSB:
You are storing value in it's own state of SelectOrInput component. You need to lift state up to parent component in order to get value in parent.
Create state in parent component and initialize with default value and create function to change it's value
const [inputValue, setInputValue] = useState(null);
const onChange = (value) => {
setInputValue(value);
};
Pass onChange function in SelectOrInput component and call onChange function whenever value is changed
<SelectOrInput
...
onChange={onChange}
/>
// call onChange in handleInputSelectChange method
const handleInputSelectChange = (
event: React.ChangeEvent<{ value: unknown }>
): void => {
const value = event.target.value as string;
if (value === "") {
const newState = !showCustomInput;
setShowCustomInput(newState);
setValue(value);
onChange(value);
} else {
setValue(value);
onChange(value);
}
};
Working example: https://codesandbox.io/s/dynamic-input-select-wk2je
With the great help of #Priyank Kachhela, I was able to find out the answer.
By Lifting the State to it's closest common ancestor as well as removing any Controller Component inside the child component.
App.js
Create state in parent component and initialize with default value and create function to change it's value
const [inputValue, setInputValue] = useState("null");
const onSubmit = (data) => {
// Stringify Object to always see real value, not the value evaluated upon first expanding.
// https://stackoverflow.com/questions/23429203/weird-behavior-with-objects-console-log
console.log(JSON.stringify(data, 4));
};
const onChange = (value) => {
setInputValue(value);
};
Wrap SelectOrInput with Controller and Pass onChange function, value as well as defaultValue to the Controller. Then use the render method and spread field on SelectOrInput Component.
<Controller
name={`contactPerson[0].salutation`}
defaultValue={defaultValues.salutation}
onChange={onChange}
value={inputValue}
control={control}
render={({ field }) => (
<SelectOrInput
{...field}
variant="outlined"
selectOptions={customSalutationOptions}
/>
)}
/>
components/SelectOrInput.js
Bubble / (Call) onChange Event Handler whenever value is changed from within the Child-(SelectOrInput) Component.
const handleInputSelectChange = (
event: React.ChangeEvent<{ value: unknown }>
): void => {
const value = event.target.value as string;
if (value === "") {
const newState = !showCustomInput;
setShowCustomInput(newState);
// Bubble / (Call) Event
onChange(value);
} else {
onChange(value);
}
};
const resetCustomInputToSelect = (event: React.MouseEvent<HTMLElement>) => {
const newState = !showCustomInput;
// Bubble / (Call) Event
onChange("null");
setShowCustomInput(newState);
};
Remove component internal Input State from the 'SelectOrInput'
Working Example
Revisions captured inside Gist
https://gist.github.com/kkroeger93/1e4c0fe993f1745a34fb5717ee2ff545/revisions

Is there a way to dynamically display multiple textfields with hooks depends on user selection? (values and onChange)

I am setting up a form, and in the form there's a material ui select field ranges from 1 to 50. How am I going to dynamically display/render multiple textfields "Firstname and Lastname" with Hooks in each text fields every time the user select a number?
Ex.
If user chose "3" from the option in the select box, there's expected to be 3 textfields "firstname and lastname" to be rendered.
This is what I have tried so far, I know how to create hooks, with specific textfield but I don't know if I will depend on the user selection.
const primary_guest_firstname = useForm('');
const primary_guest_lastname = useForm('');
const primary_guest_adult = useForm(1);
function useForm(init){
const [ value, setValue ] = useState(init);
function handleOnChange(e){
setValue(e.target.value);
}
return {
value,
onChange: handleOnChange
}
}
/** sample of select text field with options from 1 - 50... **/
<TextField
select
variant="outlined"
required
margin="dense"
fullWidth
value={primary_guest_adult.value}
onChange={primary_guest_adult.onChange}
id="guest"
label="Adult ( > 12 years)"
name="guest"
>
<MenuItem value="1">1</MenuItem>
<MenuItem value="2">2</MenuItem>
<MenuItem value="3">3</MenuItem>
...
<MenuItem value="50">50</MenuItem>
</TextField>
/** I should render dynamic textfield here...
if the user selected "3", there should be 3 textfields
generated here with hook.. **/
<TextField
variant="outlined"
required
margin="dense"
fullWidth
id="firstName"
label="First Name"
name="firstName"
value={primary_guest_firstname.value}
onChange={primary_guest_firstname.onChange}
/>
<TextField
variant="outlined"
required
margin="dense"
fullWidth
id="lastName"
label="Last Name"
name="lastName"
value={primary_guest_lastname.value}
onChange={primary_guest_lastname.onChange}
/>
I appreciate your help :) Thanks!
You can do this by creating an array of required values in the state array.
Instead of single state, you would have an array of states, which needs to be set and read accordingly.
Demo:
const { render } = ReactDOM;
const { useState, useEffect } = React;
function useForm(init){
const [ value, setValue ] = useState(init);
function handleOnChange(e){
setValue(e.target.value);
}
return {
value,
onChange: handleOnChange
}
}
function useForms(reqNum) {
const initial = [...Array(reqNum)].map(()=>({ firstName: "", lastName: "" }));
const [values, setValues] = useState(initial);
function handleChange(event, name, changedIndex) {
const { target: { value }} = event;
setValues(values => values.map((val, index) => {
if(changedIndex === index) {
return {
...val,
[name]: value,
}
}
return val;
}));
}
function handleNumChange(changedNum) {
const changedValues = [...Array(+changedNum)].map(()=>({ firstName: "", lastName: "" }));
setValues(changedValues);
}
return {
values,
handleChange,
handleNumChange,
}
}
const NameFields = ({ values }) => {
return (
<div>
{values.map((value, index) => (
<div key={index}>
<input placeholder={`First Name ${index+1}`} value={value.firstName} onChange={(e) => handleChange(e, "firstName", index)} />
<input placeholder={`Last Name ${index+1}`} value={value.lastName} onChange={(e) => handleChange(e, "lastName", index)} />
</div>
))}
</div>
);
}
const App = () => {
const options = [1,2,3,4,5];
const {value: numberOfAdults, onChange: setNumberOfAdults} = useForm(1);
const { values, handleChange, handleNumChange } = useForms(numberOfAdults);
useEffect(() => {
handleNumChange(numberOfAdults);
}, [numberOfAdults]);
return (
<main>
<select value={numberOfAdults} onChange={setNumberOfAdults}>
{options.map((option)=>(
<option value={option} key={option}>{option}</option>
))}
</select>
<NameFields values={values} />
</main>
);
}
render(<App />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />

Remove asterisk from only one field

I have a custom component that takes in values from an API and then displays them to a user, however within this component I give a 'required' flag to give an asterisk to the label, however I only want one field as seen below to have an asterisk not both as is currently happening.
<Grid item xs={12} sm={6}>
<SearchUsers
name="primaryOfficerId"
label="PO Responsible"
id="primaryOfficerId"
onSelect={change.bind(null, 'primaryOfficerId')}
error={touched.primaryOfficerId && Boolean(errors.primaryOfficerId)}
/>
</Grid>
<Grid item xs={12} sm={6}>
<SearchUsers
name="supportOfficerId"
label="Support Officer"
id="supportOfficerId"
onSelect={change.bind(null, 'supportOfficerId')}
/>
</Grid>
And now my custom component
const Search = (props) => {
const [data, setData] = useState([]);
const [select, setSelect] = useState(0);
const { type: TYPE, name: NAME, label: LABEL, onSelect, filter } = props;
const applyFilter = (data) => {
let result = data;
if (filter) {
result = filter(data);
}
return result;
};
useEffect(() => {
getLookupData(TYPE)
.then((response) => {
setData(response);
})
.catch((error) => {
if (error === HttpStatus.NOT_FOUND) {
setData([]);
} else {
throw error;
}
});
}, [TYPE]);
const options = applyFilter(data).map((item) => (
<MenuItem value={item.id} key={item.id}>
{item[NAME]}
</MenuItem>
));
const handleChange = (event) => {
setSelect(event.target.value);
onSelect && onSelect(event);
};
const { classes } = props;
return (
<FormControl required className={classes.formControl} id={NAME} error={props.error}>
<FormControlLabel control={<InputLabel htmlFor={NAME}>{LABEL}</InputLabel>} />
<Select
name={TYPE}
value={select}
onChange={handleChange}
disabled={props.disabled || options.length === 0}
input={<Input name={TYPE} id={NAME} />}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{options}
</Select>
</FormControl>
);
};
Below you can see an image of my problem, both PO, and So responsible have an asterisk. I need only PO to have this asterisk but my component does not currently allow for individuals
Simply make your component customizable by passing it a required prop as boolean then in your component make it dynamic :
<FormControl required={props.required}>
// ...
</FormControl>
So now you can use it with an asterisk <SearchUsers required /> or without <SearchUsers required={false} />
Add additional prop required to your Search component and then use it as a prop value for FormControl:
<FormControl required={props.required} />

Categories

Resources