Since I want my variation form to be dynamic, I have used the concept of field array from react-hook-form. For the select field I am using react-select which is used for taking multiple values for a particular option from the user. This is how I have done
import React from "react";
import { useForm, useFieldArray } from "react-hook-form";
import ReactSelect from "./Select";
import Input from "./Input";
import VariationPreview from "./VariationPreview";
const initialValues = {
variations: [
{
id: 1,
option: "Size",
values: [
{ id: 1, label: "SM", value: "sm" },
{ id: 2, label: "MD", value: "md" }
]
},
{
id: 2,
option: "Color",
values: [
{ id: 1, label: "Red", value: "red" },
{ id: 2, label: "Blue", value: "blue" }
]
}
]
};
const VariationSetup = () => {
const {
register,
control,
handleSubmit,
formState: { errors },
watch,
getValues
} = useForm({
defaultValues: initialValues ?? {}
});
const { fields, append, remove } = useFieldArray({
control, // control props comes from useForm (optional: if you are using FormContext)
name: "variations" // unique name for your Field Array
});
const variants = watch("variations");
console.log("variants", variants);
return (
<>
<p className="font-semibold mb-4 text-md">Options</p>
{fields.map((field, index) => {
return (
<React.Fragment key={field.id}>
<div className="flex mb-4" key={field.id}>
<div className="w-full md:w-4/12">
<Input
name={`variations.${index}.option`}
label="Option"
placeholder="Choose option"
options={VARIATION_OPTION}
helperText="Choose option that can be applied as variants for a product"
errors={errors}
register={register}
/>
</div>
<div className="w-full md:ml-4 md:w-8/12">
<ReactSelect
name={`variations.${index}.values`}
label=""
placeholder="Choose value"
options={[]}
helperText="You can choose multiple values"
wrapperClassName="mt-7"
errors={errors}
register={register}
isCreateable
/>
</div>
</div>
</React.Fragment>
);
})}
<button
className="bg-gray-200 p-3"
type="button"
onClick={() => append({ option: "", value: "" })}
>
Add option
</button>
<div className="divide-y"></div>
{/* PREVIEW */}
<VariationPreview variations={watch("variations")} />
</>
);
};
export default VariationSetup;
const VARIATION_OPTION = [
{ id: 1, label: "Size", value: "size" },
{ id: 2, label: "Color", value: "color" }
];
this is the react-select with hook-form binding
import { Controller, useForm } from "react-hook-form";
import Select from "react-select";
import CreatableSelect from "react-select/creatable";
export default function ReactSelect({
disabled,
label,
helperText,
name,
placeholder,
options,
defaultValue,
className,
labelClassName,
wrapperClassName,
isCreateable = false
}) {
const {
control,
formState: { errors }
} = useForm();
const getDefaultValue = (value) => {
if (value && value.length) {
return value[0];
} else return value;
};
return (
<div className={wrapperClassName ? wrapperClassName : ""}>
{/* TODO: Label as a separate component */}
<label
htmlFor={name}
className={
labelClassName
? labelClassName
: "block font-semibold mb-2 text-gray-700 text-sm tracking-wide uppercase"
}
>
{label}
</label>
<div className="mt-1 relative">
<Controller
name={name}
defaultValue={getDefaultValue(defaultValue)}
control={control}
render={({ field }) => {
const styles = errors[name] ? errorStyles : customStyles;
if (isCreateable) {
return (
<CreatableSelect
isMulti
{...field}
isDisabled={disabled}
placeholder={placeholder}
options={options}
styles={styles}
className={className}
/>
);
} else {
return (
<Select
{...field}
isDisabled={disabled}
placeholder={placeholder}
options={options}
styles={styles}
className={className}
/>
);
}
}}
/>
</div>
</div>
);
}
Here the problem is when I update values field the variations object does not get updated because of which I cannot update Variation Preview table. Also when if I add new option, the option key gets updated and is reflected in VariationPreview table but when I add values for that option the values object is shown empty. Could anyone point me where i did the mistake? I have a code in playground either
https://codesandbox.io/s/elegant-robinson-fiujlj?file=/src/Variation.jsx:0-2764
You call useForm inside the ReactSelect component. Now there are two unrelated forms. That's why variations object does not get updated.
You can pass the control down to ReactSelect and use it for Controller component.
And for errors handling:
const { errors } = useFormState({
control
});
Related
I made a reusable formik form, but I want to use ant select instead of formik select. the Error message is not working with ant design and I dont know how to configure that.
I need to show the error message when it's not validated. in console.log there is no problem it wworks perfect as i change the ant select. But the errortext does not shows
import React from "react";
import { Formik } from "formik";
import {Form} from 'antd'
import * as Yup from "yup";
import FormikController from "../components/Forms/FormikController";
const city = [
{ key: "option 1", value: "option1" },
{ key: "option 2", value: "option2" },
{ key: "option 3", value: "option3" },
{ key: "option 4", value: "option4" },
];
const Contact = () => {
const initialValues = {
whoYouAre: "",
email: "",
message: "",
};
const validationSchema = Yup.object({
whoYouAre :Yup.string().required(),
email :Yup.string().required()
});
return (
<Container className="contact">
<h5 className="mb-4">Contact</h5>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
// onSubmit={handleAddData}
validateOnMount
>
{(formik) => (
<Form className="w-100">
<FormikController
control="selectsearch"
options={city}
formik={formik}
name="whoYouAre"
label="Please Tell Us Who You Are"
/>
<FormikController
control="input"
type="text"
name="email"
label="Email"
/>
<FormikController
control="textarea"
name="message"
rows="8"
label="Message"
/>
<div>
<Button variant="primary" type="submit">
Send
</Button>
</div>
</Form>
)}
</Formik>
</Container>
);
};
export default Contact;
and here the resusable select that i used from ant design
import React from "react";
import { Form,Select } from "antd";
import { ErrorMessage } from "formik";
import TextError from "./TextError";
const { Option } = Select;
const SearchSel = (props) => {
const { label, name, options ,formik, ...rest } = props;
console.log(formik);
return (
<Form.Item className="mb-3">
<Select
showSearch
size="large"
optionFilterProp="children"
placeholder={label}
name={name}
onChange={(value) => formik.setFieldValue("whoYouAre", value)}
>
{options.map((option) => {
return (
<Option key={option.value} value={option.value}>
{option.key}
</Option>
);
})}
</Select>
<ErrorMessage name={name} component={TextError} />
</Form.Item>
);
};
export default SearchSel;
Your default state is "" isn't right I believe. You probably want null.
Secondly, your Select is not correctly bound to the form state. You need to add value.
Moreover, you also need to add defaultActiveFirstOption={false} as that may be immediately selecting the first item on the initial state.
Finally, and probably most importantly, probably need to bind the fields blur such that it sets the field as touched. ErrorMessage won't show the error unless it thinks the user touched the field:
<Select
showSearch
size="large"
optionFilterProp="children"
placeholder={label}
name={name}
value={formik.values.whoYouAre}
defaultActiveFirstOption={false}
onChange={(value) => formik.setFieldValue("whoYouAre", value)}
onBlur={() => formik.setFieldTouched("whoYouAre", true)}
>
I have this data where every time a user wants to add more color, it will add colors in the field color. Something like this: color: ["blue","green","yellow"]or an array. As of now, if I'll add more colors, it will just override the first color.
How can I update the field color without overriding the previous values?
index.js
import React from "react";
import { useForm } from "react-hook-form";
import FieldArray from "./fieldArray";
import ReactDOM from "react-dom";
import "./styles.css";
import { Button } from "#mui/material";
const defaultValues = {
test: [
{
product: "",
nestedArray: [{ size: "", color: "", design: "" }]
}
]
};
function App() {
const {
control,
register,
handleSubmit,
getValues,
errors,
reset,
setValue
} = useForm({
defaultValues
});
const onSubmit = (data) => console.log("data", data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h1>Array of Array Fields</h1>
<p>
The following example demonstrate the ability of building nested array
fields.
</p>
<FieldArray
{...{ control, register, defaultValues, getValues, setValue, errors }}
/>
<button type="button" onClick={() => reset(defaultValues)}>
Reset
</button>
<Button type="submit">Submit</Button>
{/* <input type="submit" /> */}
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
fieldArray.js
import React from "react";
import { useFieldArray } from "react-hook-form";
import NestedArray from "./nestedFieldArray";
import { InputLabel } from "#mui/material";
import Size from "./Drop_drowns/Size";
let renderCount = 0;
export default function Fields({ control, register, setValue, getValues }) {
const { fields, append, remove, prepends } = useFieldArray({
control,
name: "test"
});
renderCount++;
const productItems = [
{ label: "Shirt1", value: "Shirt1" },
{ label: "Shirt2", value: "Shirt2" },
{ label: "Shirt3", value: "Shirt3" },
{ label: "Shirt4", value: "Shirt4" }
];
const menuItems = [
{ label: "S", value: "S" },
{ label: "M", value: "M" },
{ label: "L", value: "L" }
];
return (
<>
<ul>
{fields.map((item, index) => {
return (
<li key={item.id}>
<label>Item {index + 1}</label>
<InputLabel id="demo-simple-select-label">Product</InputLabel>
<Size
name={`test[${index}].product`}
menuItems={productItems}
refer={register({ required: true })}
defaultValue={item.product}
control={control}
/>
<InputLabel id="demo-simple-select-label">Size</InputLabel>
<Size
name={`test[${index}].size`}
menuItems={menuItems}
refer={register({ required: true })}
defaultValue={item.size}
control={control}
/>
<InputLabel id="demo-simple-select-label">color</InputLabel>
<Size
name={`test[${index}].color`}
menuItems={menuItems}
refer={register({ required: true })}
defaultValue={item.color}
control={control}
/>
<button type="button" onClick={() => remove(index)}>
Delete
</button>
<NestedArray nestIndex={index} {...{ control, register }} />
</li>
);
})}
</ul>
<section>
<button
type="button"
onClick={() => {
append({ name: "append" });
}}
>
Add product
</button>
</section>
<span className="counter">Render Count: {renderCount}</span>
</>
);
}
nestedFieldArray
import React from "react";
import { useFieldArray } from "react-hook-form";
import Size from "./Drop_drowns/Size";
import { InputLabel } from "#mui/material";
//only changed here the name nestedArray to variations
export default ({ nestIndex, control, register }) => {
const { fields, remove, append } = useFieldArray({
control,
name: `test[${nestIndex}].variantion`
});
const menuItems = [
{ label: "S", value: "S" },
{ label: "M", value: "M" },
{ label: "L", value: "L" }
];
const colorItems = [
{ label: "red", value: "red" },
{ label: "green", value: "green" },
{ label: "blue", value: "blue" }
];
return (
<div>
{fields.map((item, k) => {
return (
<div key={item.id} style={{ marginLeft: 20 }}>
{/* <Size
name={`test[${nestIndex}].variantion[${k}].size`}
menuItems={menuItems}
refer={register({ required: true })}
defaultValue={item.size}
control={control}
/> */}
<InputLabel id="demo-simple-select-label">Color</InputLabel>
<Size
name={`test[${nestIndex}].color`}
menuItems={colorItems}
refer={register({ required: true })}
defaultValue={item.color}
control={control}
/>
{/* <input
name={`test[${nestIndex}].variantion[${k}].color`}
ref={register({ required: true })}
defaultValue={item.field}
style={{ marginRight: "25px" }}
/> */}
<button type="button" onClick={() => remove(k)}>
Delete Colors
</button>
</div>
);
})}
<button
type="button"
onClick={() =>
append({
field1: "field1",
field2: "field2"
})
}
>
Add Colors
</button>
<hr />
</div>
);
};
Seems like what you're trying to achieve with colors is providing one or more options the user can choose from. The way it is now with adding a field for each may not be the best approach.
A more common method would be checkboxes where you can select one or many. Here's a blog post about this in React. While this discusses a cash register, the concept of adding the number values can be applied to the color values to form an array of the ones selected.
If you would like to continue using fields, the root cause of the issue in your current code is that each field for a color is mapped to the color in the output object, so if there's multiple the last one wins. There are a few ways to go about fixing this:
bring in a new dependency like Field.Group component of React Advanced Form or something like react-fieldset to modify color to group/nest the fields underneath it
write your own submit handleSubmit function where you could retrieve and combine the color fields into one
remove the concept of a singular color and have it be numeric (e.g. color1, color2, etc.)
In defaultValues change nestedArray as map in which declare color viables as array and something like this...
var defaultValues = {
test: [
{
product: "",
nestedArray: { size: ["S","M","L"], color: ["blue","green","yellow"], design: "" }
}
]
};
once user adds color add new color value in that color Array.
If you want to save new values and do not override old one, use color as a list and append to it. Save all values to list. Use all values or one specific.
const arr = [
"blue"
];
//add new value
arr.push("red")
console.log("VALUES", arr);
console.log("FIRST ITEM", arr[0]);
console.log("LAST ITEM", arr[arr.length - 1])
I am a bit confused, here is an example with a couple of select inputs that have the same state, please check here: https://stackblitz.com/edit/get-selected-by-value-multi-select-react-agamk4?file=src/App.js so please:
How can I make it so when I select an option the value does not apply to the rest of the select inputs?
How would you put the values in the store for each of the selects?
Do I need multiple stores?
For more clarity, here is a screenshot: https://www.awesomescreenshot.com/image/19798040?key=bb839c650c93b436066e03d33d5515b0 I hope this makes sense? What would be the best approach? Thank you.
I have shared the code in case of only a single state. You can use this method if you want only a single state but having multiple states for different select inputs also won't be bad as you have only 3 inputs. Having single state method would be useful if number of select inputs would have more.
import React, { useState } from 'react';
import Select from 'react-select';
function App() {
const data = [
{
value: 1,
label: 'cerulean',
},
{
value: 2,
label: 'fuchsia rose',
},
{
value: 3,
label: 'true red',
},
{
value: 4,
label: 'aqua sky',
},
{
value: 5,
label: 'tigerlily',
},
{
value: 6,
label: 'blue turquoise',
},
];
// set value for default selection
const [selectedValue, setSelectedValue] = useState([
{ value: [] },
{ value: [] },
{ value: [] },
]);
// handle onChange event of the dropdown
const handleChange = (e, no) => {
setSelectedValue(
selectedValue.map((item) => {
return selectedValue.indexOf(item) === no
? { value: Array.isArray(e) ? e.map((x) => x.value) : [] }
: item;
})
);
};
return (
<div className="App">
<Select
className="dropdown"
placeholder="Select Option"
value={data.filter((obj) => selectedValue[0].value.includes(obj.value))} // set selected values
options={data} // set list of the data
onChange={(event) => handleChange(event, 0)} // assign onChange function
isMulti
isClearable
/>
<br />
<Select
className="dropdown"
placeholder="Select Option"
value={data.filter((obj) => selectedValue[1].value.includes(obj.value))} // set selected values
options={data} // set list of the data
onChange={(event) => handleChange(event, 1)} // assign onChange function
isMulti
isClearable
/>
<br />
<Select
className="dropdown"
placeholder="Select Option"
value={data.filter((obj) => selectedValue[2].value.includes(obj.value))} // set selected values
options={data} // set list of the data
onChange={(event) => handleChange(event, 2)} // assign onChange function
isMulti
isClearable
/>
{selectedValue && (
<div style={{ marginTop: 20, lineHeight: '25px' }}>
<div>
<b>Selected Value: </b> {JSON.stringify(selectedValue, null, 2)}
</div>
</div>
)}
</div>
);
}
export default App;
{selectedValue && (
<div style={{ marginTop: 20, lineHeight: '25px' }}>
<div>
<b>Selected Values: </b>
<span>{
selectedValue.map(item => item.value.length !== 0 ?
<li>{data.filter(data => data.value === item.value[0])[0].label}</li> :
<li>No value selected</li>
)
}</span>
</div>
</div>
)}
const [application, setApplication] = useState([])
const [app, setApp] = useState([
{
id: null,
code: null,
name: null
}
]);
useEffect(() => {
let ignore = false;
(async function load() {
let response = await getAllData();
if (!ignore) setApplication(response['data'])
})()
return () => ignore = true;
},[]);
{
label: (
<div className="flex items-center">
<label className="flex-1">Application</label>
<div className="text-right">
<ButtonGroup>
<IconButton icon={<Icon icon="plus" />} onClick={() => appendApp()} />
<IconButton onClick={() => removeApp()} size="md" icon={<Icon icon="minus" />} style={{ display: app.length > 1 ? 'inline-block' : 'none' }} />
</ButtonGroup>
</div>
</div>
),
name: 'applications',
renderer: (data) => {
const { control, register, errors } = useFormContext();
return (
<div className="flex flex-col w-full">
{
app.map((item, index) => (
<div key={index} className="flex flex-col pb-2 -items-center">
<div className="flex pb-2 w-full">
<SelectPicker
placeholder="Select Application"
data={application['data']}
labelKey="name"
valueKey="code"
style={{ width: '100%' }}
disabledItemValues={Array.isArray(control.getValues()['applications']) ? control.getValues()['applications'].map(x => x.id) : []}
onChange={(value) => control.setValue('applications', _setApp(value, index, 'code'))}
value={control.getValues()['applications']?.code}
/>
</div>
</div>
))
}
</div>
)
}
const appendApp = () => {
let i = 0;
for (i = 0; i < noOfApp; i++) {
setApp(arr => [...arr, {
id: null,
code: null,
name: null,
role: null
}]);
return app;
}
}
const removeAppRole = () => {
setApp([...app.slice(0, -1)]);
}
const _setApp = (value, idx, status) => {
app[idx][status] = value;
setApp(app);
return app;
}
How do I add a validation on the select? for example when the select field is empty it should validation that it is required to select. also for example when there's a existing data which is like this:
data = [{
id: 1,
name: 'IOS',
code: 'ios'
}]
and how do I display this data on the select field? cause I have a create and edit.
when I try to edit it doesn't display the value.
I do not use the register, I prefer to use Controller, for these cases it is more practical, in the v7 of react-hook-form, see this example:
My select component:
import { ErrorMessage } from "#hookform/error-message";
import { IonItem, IonLabel, IonSelect, IonSelectOption } from "#ionic/react";
import { FunctionComponent } from "react";
import { Controller } from "react-hook-form";
import React from "react";
const Select: FunctionComponent<Props> = ({
options,
control,
errors,
defaultValue,
name,
label,
rules
}) => {
return (
<>
<IonItem className="mb-4">
<IonLabel position="floating" color="primary">
{label}
</IonLabel>
<Controller
render={({ field: { onChange, onBlur, value } }) => (
<IonSelect
value={value}
onIonChange={onChange}
onIonBlur={onBlur}
interface="action-sheet"
className="mt-2"
>
{options ? options.map((opcion) => {
return (
<IonSelectOption value={opcion.value} key={opcion.value}>
{opcion.label}
</IonSelectOption>
);
}):""}
</IonSelect>
)}
control={control}
name={name}
defaultValue={defaultValue}
rules={rules}
/>
</IonItem>
<ErrorMessage
errors={errors}
name={name}
as={<div className="text-red-600 px-6" />}
/>
</>
);
};
export default Select;
Use the component in other component:
import Select from "components/Select/Select";
import { useForm } from "react-hook-form";
import Scaffold from "components/Scaffold/Scaffold";
import React from "react";
let defaultValues = {
subjectId: "1"
};
const options = [
{
label: "Option1",
value: "1",
},
{
label: "Option2",
value: "2",
},
];
const ContactUs: React.FC = () => {
const {
control,
handleSubmit,
formState: { isSubmitting, isValid, errors },
} = useForm({
defaultValues: defaultValues,
mode: "onChange",
});
const handlerSendButton = async (select) => {
console.log(select);
};
const rulesSubject = {
required: "this field is required",
};
return (
<Scaffold>
<Scaffold.Content>
<h6 className="text-2xl font-bold text-center">
Contact us
</h6>
<Select
control={control}
errors={errors}
defaultValue={defaultValues.subjectId}
options={options}
name="subjectId"
label={"Subject"}
rules={rulesSubject}
/>
</Scaffold.Content>
<Scaffold.Footer>
<Button
onClick={handleSubmit(handlerSendButton)}
disabled={!isValid || isSubmitting}
>
Save
</Button>
</Scaffold.Footer>
</Scaffold>
);
};
In this case i use Ionic for the UI but you can use MaterialUI, ReactSuite o other framework, its the same.
I hope it helps you, good luck.
EDIT
A repository : Ionic React Select Form Hook
A codeSandBox: Ionic React Select Form Hook
I working on a react project where I have requirement like,
I have array inside contain, 1 Object and 1 Array named Task[]
"contractor": [
{
"contractGivenBy": -1,
"contractorID": 0,
"contractorName": "contractor1",
"reviewedByAssigner": false,
"submitReviewToAssigner": false,
"tasks": [ 2, 4, 6 ],
"tasksDone": false
},
Now, I want to display the Tasks array as Checkboxes in the page.
That is nice, I displayed all checkboxes using map() method, But the problem is, How to handle (get values from those checkboxes) when user checked or unchecked the specific checkbox.
I'm using React functional component with React hooks.
Here is what is tried..
<form onSubmit={onSubmitHandler}>
{
projectData.contractor[0].tasks.map((task, index) => {
return (
<div style={{ flexDirection: "column" }}>
<FormControlLabel
control={
<Checkbox
checked={false}
value={task}
onChange={handleTask} />
}
label={`task ${task}`}
/>
</div>
)
})
}
<Button
type="submit"
style={{
backgroundColor:"rgba(25,123,189)",
color: "white"
}}>
Assgin
</Button>
</form>
UPDATED
Here you go , it uses react hooks with checkbox implementation, i have tweaked it a little with <input type /> but you will get the idea
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Checkbox = ({ type = "checkbox", name, checked = false, onChange }) => {
console.log("Checkbox: ", name, checked);
return (
<input type={type} name={name} checked={checked} onChange={onChange} />
);
};
const CheckboxExample = () => {
const [checkedItems, setCheckedItems] = useState({});
const handleChange = event => {
setCheckedItems({
...checkedItems,
[event.target.name]: event.target.checked
});
console.log("checkedItems: ", checkedItems);
};
const checkboxes = [
{
name: "check-box-1",
key: "checkBox1",
label: "Check Box 1"
},
{
name: "check-box-2",
key: "checkBox2",
label: "Check Box 2"
}
];
return (
<div>
<lable>Checked item name : {checkedItems["check-box-1"]} </lable> <br />
{checkboxes.map(item => (
<label key={item.key}>
{item.name}
<Checkbox
name={item.name}
checked={checkedItems[item.name]}
onChange={handleChange}
/>
</label>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<CheckboxExample />, rootElement);