I'm trying to create a custom hook that'll can be used in any form but i have a problem which i am not sure of what it is. whenever i start typing into the fields it loses focus, to type into it again you'd have to click into it and it goes on like that. this isn't good and i would appreciate it if anyone can help me solve this error.
useForm.js
import { useState } from "react";
const useForm = () => {
const [values, setValues] = useState({
// contact form
});
const [errors, seterrors] = useState({});
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
// console.log(name, value);
};
const validate = (data) => {};
const handleSubmit = (e) => {
e.preventDefault();
// console.log(e);
};
// console.log(values);
return {
handleChange,
values,
handleSubmit
};
};
export default useForm;
Form.js
import InputGroup from "./InputGroup";
import useForm from "./useForm";
const Form = () => {
const fields = [
{
label: "First Name",
className: "input-group",
name: "firstName",
placeholder: "John",
type: "text"
},
{
label: "Last Name",
className: "input-group",
name: "lastName",
placeholder: "Doe",
type: "text"
},
{
label: "Email",
className: "input-group",
name: "email",
placeholder: "JohnDoe#example.com",
type: "email"
},
{
label: "Phone Number",
className: "input-group",
name: "Phone",
placeholder: "+234 (0)81 234 5678",
type: "text"
// pattern: "/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-s./0-9]*$/g"
},
{
label: "Comments",
className: "input-group",
name: "comment",
placeholder: "Message",
type: "textarea"
}
];
const { values, handleChange, handleSubmit } = useForm();
//funtion to generate id based on fields lenght
const id = () => {
const head = Date.now.toString(36);
const tail = Math.random().toString(36).substr(2);
return head + tail;
};
console.log(id);
return (
<form onSubmit={handleSubmit}>
{fields.map((field) => (
<InputGroup
key={id}
value={values}
onChange={handleChange}
{...field}
/>
))}
<button>submit</button>
</form>
);
};
export default Form;
Input
const Input = ({ ...props }) => {
return <input {...props} />;
};
export default Input;
You are not using your id function properly, you are only referring to it, not invoking it, change -
<InputGroup
key={id}
to
<InputGroup
key={id()}
or better still, a better name, using a verb would have highlighted the problem easier, as it now looks like a function and not a variable
<InputGroup
key={buildId()}
or whatever, but better still, you should use a static unique value for the key (i.e unique but would not change between renders, so using timestamps is a bad idea), so you could add a unique id to each field and use it -
const fields = [
{
id: <some unique ID>, <!---- here
label: "First Name",
className: "input-group",
name: "firstName",
placeholder: "John",
type: "text"
}
...
]
...
<InputGroup
key={field.id}
Related
I'm strugling with my form in typescript react. Basicly I created 2 simple components Button and Input and add them to the another component Form where I created a whole form.
After submit I should get something like this:
telephone: "323423234"
email: "tress#wess.com"
message: "weklfnwlkf"
name: "less"
surname: "mess"
But after submit I get weird response like this:
"": "323423234"
email: "tress#wess.com"
message: "weklfnwlkf"
name: "less"
surname: "mess"
telephone: ""
It is a little bit werido because I done something like this couple days ago and now idk what to do.
There is sample of my Form code
const Form = () => {
interface FormDataType {
telephone: string;
name: string;
surname: string;
email: string;
message: string;
}
const formData: FormDataType = {
telephone: '',
name: '',
surname: '',
email: '',
message: '',
};
const [responseBody, setResponseBody] = useState<FormDataType>(formData);
const clearState = () => {
setResponseBody({ ...formData });
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setResponseBody({ ...responseBody, [name]: value });
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(responseBody);
clearState();
};
return (
<>
<form onSubmit={handleSubmit}>
{FormData.map((option) => (
<Input
key={option.name}
label={option.label}
type={option.type}
className={option.className}
name={option.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleChange(e)
}
/>
))}
<div className='div'>
<Button
type='submit'
title={'Simple test'}
className={'btn-primary'}
/>
</div>
</form>
</>
);
};
export default Form;
And when I'm invoking in handleSubmit function clearState() it doesn't refresh states in webpage inputs.
I hope this is what it needs, I made some changes, but you can adapt it to your project
export default function App() {
interface FormDataType {
telephone: string;
name: string;
surname: string;
email: string;
message: string;
}
const formData: FormDataType = {
telephone: "",
name: "",
surname: "",
email: "",
message: ""
};
const [values, setValues] = useState<FormDataType | any>(formData);
// reset
const reset = (newFormState = formData) => {
setValues(newFormState);
};
// handleInputChange
const handleInputChange = ({ target }: any) => {
setValues({ ...values, [target.name]: target.value });
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(values);
reset();
};
const data = [
{
type: "text",
name: "name"
},
{
type: "text",
name: "surname"
},
{
type: "email",
name: "email"
},
{
type: "tel",
name: "telephone"
},
{
type: "text",
name: "message"
}
];
return (
<>
<form onSubmit={handleSubmit}>
{data.map((option) => (
<input
key={option.name}
value={values[option.name]}
type={option.type}
name={option.name}
placeholder={option.name}
onChange={handleInputChange}
/>
))}
<div className="div">
<button type="submit">Simple test</button>
</div>
</form>
</>
);
}
Your reset doesn't work because your inputs are uncontrolled.
To have them reset properly, you should make them controlled by passing them a value prop. It would look something like this:
<Input
key={option.name}
label={option.label}
type={option.type}
className={option.className}
name={option.name}
value={responseBody[option.name]} // Add this
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
handleChange(e)
}
/>
This will connect the responseBody form data with the input values.
As to the odd console logged value, it would appear that at least one of the option.name values is empty/undefined (looks like the telephone object). Verify that the FormData contains complete and correct values.
I'm trying to insert a task with multiple fields like title, text, and tag only in the 'To Do' category. It would be an object inside an array, inside an object. The way I'm implementing it is totally wrong, and it's generating a chain of objects inside another object. How could I be performing so that the data looks the same as in the Table?
This is the way I need the data to be saved:
const Table = [
{
id: uuidv4(),
title: "To do",
tasks: [
{
id: uuidv4(),
title: "Learn JavaScript",
text: "lorem ipsum",
tag: "tag1",
},
{
id: uuidv4(),
title: "Learn Git",
text: "lorem ipsum",
tag: "tag2",
},
],
},
{
id: uuidv4(),
title: "Doing",
tasks: [
{
id: uuidv4(),
title: "Learn CSS",
text: "lorem ipsum",
tag: "tag1",
},
],
},
{
id: uuidv4(),
title: "Done",
tasks: [
{
id: uuidv4(),
title: "Learn HTML",
text: "lorem ipsum",
tag: "tag1",
},
],
},
];
This is the code I made
import React, { useState } from "react";
import Input from "./Input";
import "./App.css";
const App = () => {
const [tasks, setTasks] = useState([]);
function addTask(task) {
const newTask = { ...task };
newTask[0] = {
...tasks[0],
arrayTasks: task,
};
setTasks([...tasks, newTask]);
}
return (
<div>
<Input onAdd={addTask} />
</div>
);
};
export default App;
and this one:
import { useState } from "react";
const Input = ({ onAdd }) => {
const [title, setTitle] = useState("");
const [text, setText] = useState("");
const [tag, setTag] = useState("");
const [status, setStatus] = useState("ToDo");
const onSubmit = (e) => {
e.preventDefault();
if (!text || !title) {
alert("Please enter a task with a title");
return;
}
const number = Math.floor(Math.random() * 10000) + 1;
const id = "" + number;
onAdd({ title, text, tag, status, id });
setTitle("");
setText("");
setTag("");
setStatus("ToDo");
};
return (
<div>
<input
type="text"
placeholder="Add Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<input
type="text"
placeholder="Add Task"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<input
type="text"
placeholder="Insert you tag"
value={tag}
onChange={(e) => setTag(e.target.value)}
/>
<button type="submit" onClick={onSubmit}>
Save Task
</button>
</div>
);
};
export default Input;
This is the output
if you expect your task state to be like the table array.
so i think you need first to refer to which item in the array to update.
so i suggest you need to pass an extra param to the function 'id of the target item or row'.
and so, you can try something like this.
// the second param is the id of target row or item which contain the tasks obj.
// 1. take an instance of the original array
// 2. get the index of the object you want to update
// 3. return the rest of the array without the target object we need to update.
// 4. access the target object we need to update
// 5. add your tasks to the target object
// 6. return the rest of the array with the updated object.
function addTask(task, id) {
const tasksCopy = [...tasks]; // should be a copy of table variable
const targetObjIndex = tasksCopy.findIndex((tbl) => tbl.id === id);
const resetOfTheArray = tasksCopy.filter((tbl) => tbl.id !== id);
let targetItem = tasksCopy[targetObjIndex];
targetItem = { ...targetItem, tasks: [...targetItem.tasks, task] };
return [...resetOfTheArray, targetItem];
}
I am creating a dynamically generated form which reads a file from a template. I have an array of "questions" which are mapped onto a react-hook-form. The defaultValues I am using is an array of objects. The issue I am having is using react-select inside of a react-hook-form controller because it does not set the form state if it is an array. In my testing it works without issue if default values is not an array. Please see my MRE code example
const questions = [
{
value: '',
}];
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
export default function Form()
{
const methods = useForm({
defaultValues: questions,
});
const onSubmit = (data: any) => console.log(data);
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Controller
name="0.value"
control={methods.control}
render={({ field }) => (
<Select
{...field}
value={options.find((c) => c.value === field.value)}
options={options}
onChange={(e) => field.onChange(e?.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
What would be the best way to get react-hook-form controllers components like react-select to work with an array of defaultValues
The issue you are having is due to your default values being an array instead of an object. You should just wrap your questions in an object with a key of your choice. Please see example.
const questions = [
{
value: '',
}];
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
export default function Form()
{
const methods = useForm({
defaultValues: {questions},
});
const onSubmit = (data: any) => console.log(data);
return (
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Controller
name="questions.0.value"
control={methods.control}
render={({ field }) => (
<Select
{...field}
value={options.find((c) => c.value === field.value)}
options={options}
onChange={(e) => field.onChange(e?.value)}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
I used
react-quill for one of form element.
react-hook-form form validation
yup/yup resolver for validation schema
Normal inputs and textareas working as expected, but validation for react-quill is not working.
These are my code snippets.
Custom react-quill wrapper element
import React, { useRef, useState } from "react";
import PropTypes from "prop-types";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import "react-quill/dist/quill.bubble.css";
import "react-quill/dist/quill.core.css";
function Editor(props) {
const [theme, setTheme] = useState("snow");
const { id, value, inputRef, placeholder, onChange } = props;
return (
<ReactQuill
id={id}
ref={inputRef}
theme={theme}
onChange={onChange}
value={value}
modules={{
toolbar: {
...Editor.modules.toolbar,
handlers: {
// image: handleImageUpload,
},
},
...Editor.modules,
}}
formats={Editor.formats}
bounds={".app"}
placeholder={placeholder ?? ""}
/>
);
}
/*
* Quill modules to attach to editor
* See https://quilljs.com/docs/modules/ for complete options
*/
Editor.modules = {
toolbar: [
// [{ header: '1' }, { header: '2' }, { font: [] }],
// [{ size: [] }],
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
["link", "image", "video"],
["clean"],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
],
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
};
/*
* Quill editor formats
* See https://quilljs.com/docs/formats/
*/
Editor.formats = [
"header",
"font",
"size",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"indent",
"link",
"image",
"video",
];
/*
* PropType validation
*/
Editor.propTypes = {
placeholder: PropTypes.string,
};
export default Editor;
This is form component
const validationSchema = Yup.object().shape({
companyName: Yup.string().required("Company Name is required"),
... ...
jobDescription: Yup.string().required("Job description is required"), // react-quill
... ...
howtoApply: Yup.string().required("This field is required"),
applyUrl: Yup.string().required("This field is required"),
applyEmail: Yup.string().required("This field is required"),
});
const formOptions = { resolver: yupResolver(validationSchema) };
const { register, handleSubmit, reset, control, formState } = useForm(
formOptions
);
useEffect(() => {
console.log("formState", formState); // log for form value changes
});
... ... ...
<Controller
control={control}
name="jobDescription"
// rules={{
// required: "Description must have some content.",
// validate: (value) => {
// console.log("Controller", value);
// return (
// value.split(" ").length > 10 ||
// "Enter at least 10 words in the body."
// );
// },
// }}
render={({
field: { onChange, onBlur, value, name, ref },
fieldState: { invalid, isTouched, isDirty, error },
formState,
}) => (
<Editor
onChange={(description, delta, source, editor) => {
console.log(
"onChange:description",
description,
);
console.log("inputRef", ref);
onChange(description);
}}
value={value || ""}
inputRef={ref}
theme="snow"
id="jobDescription"
/>
)}
/>
I checked these issues,
https://github.com/zenoamaro/react-quill/issues/635
https://github.com/react-hook-form/react-hook-form/discussions/3372
but still not working.
Current behavior
I confirmed the changed markdown logged in console (onChange function in Editor), but can't see formState log of useEffect hook upon editor change.
(regardless of add rules prop to Controller or not)
Any help would be much appreciated
Thank you
export default function App() {
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors }
} = useForm();
useEffect(() => {
register("emailContent", { required: true, minLength: 15 });
}, [register]);
const onEditorStateChange = (editorState) => {
setValue("emailContent", editorState);
};
const onSubmit = (data) => {
console.log(data);
};
const editorContent = watch("emailContent");
return (
<div className="App">
<ReactQuill
theme="snow"
value={editorContent}
onChange={onEditorStateChange}
/>
<p className="Error">{errors.emailContent && "Enter valid content"}</p>
<input type="submit" onClick={handleSubmit(onSubmit)} />
</div>
);
}
I have created a code sandbox with a working example for react-hook-form. Here is the link:
https://codesandbox.io/s/quill-with-react-hook-form-validation-cdjru
<Controller
name="desc"
control={control}
rules={{
required: "Please enter task description",
}}
theme="snow"
modules={modules}
render={({ field }) => (
<ReactQuill
{...field}
placeholder={"Write Description"}
onChange={(text) => {
field.onChange(text);
}}
/>
)}
/>;
You Can Add Validator As ur like
I have a parent componenet called FormLeadBuilderEdit, and it is use useState hook I pass the setState(setCards in my case) function down to to my child componenet called Card. For some reason in the child componenet when I call setCard("vale") the state doesnt update. So I am not sure what I am doing wrong.
Any help would be great
Thanks
FormLeadBuilderEdit Component
import React, { useState, useEffect } from "react";
import { Card } from "./Card";
const FormLeadBuilderEdit = ({ params }) => {
const inputs = [
{
inputType: "shortText",
uniId: Random.id(),
label: "First Name:",
value: "Kanye",
},
{
inputType: "phoneNumber",
uniId: Random.id(),
label: "Cell Phone Number",
value: "2813348004",
},
{
inputType: "email",
uniId: Random.id(),
label: "Work Email",
value: "kanye#usa.gov",
},
{
inputType: "address",
uniId: Random.id(),
label: "Home Address",
value: "123 White House Avenue",
},
{
inputType: "multipleChoice",
uniId: Random.id(),
label: "Preferred Method of Contact",
value: "2813348004",
multipleChoice: {
uniId: Random.id(),
options: [
{
uniId: Random.id(),
label: "Email",
checked: false,
},
{
uniId: Random.id(),
label: "Cell Phone",
checked: false,
},
],
},
},
{
inputType: "dropDown",
uniId: Random.id(),
label: "How did you find us?",
value: "2813348004",
dropDown: {
uniId: Random.id(),
options: [
{
uniId: Random.id(),
label: "Google",
},
{
uniId: Random.id(),
label: "Referral",
},
],
},
},
];
const [cards, setCards] = useState([]);
setCards(inputs)
return (
<>
<Card
key={card.uniId + index}
index={index}
id={card.uniId}
input={card}
setCards={setCards}
params={params}
cards={cards}
/>
</>
);
};
export default FormLeadBuilderEdit;
Cart Component
import React, { useRef } from "react";
import { Random } from "meteor/random";
export const Card = ({ setCards, cards }) => {
const addOption = () => {
const newCards = cards;
newCards.map((card) => {
if (card.inputType === "multipleChoice") {
card.multipleChoice.options.push({
uniId: Random.id(),
label: "test",
checked: false,
});
}
});
console.log(newCards);
setCards(newCards);
return (
<>
<button onClick={addOption} type="button">
Add Option
</button>
</>
);
};
React uses variable reference as a way to know which state has been changed and then triggers re-renders.
So one of the first thing you would like to know about state is that "Do not mutate state directly".
Reference: https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
Instead, produce a new state that contains changes (which has different variable reference) :
const addOption = () => {
const newCards = cards.map((card) => {
if (card.inputType === "multipleChoice") {
const newOption = {
uniId: Random.id(),
label: "test",
checked: false,
};
card.multipleChoice.options = [...card.multipleChoice.options, newOption];
}
return card;
});
setCards(newCards);
// setCards(cards); <- this refer to the current `cards` which will not trigger re-render
};
As pointed out by one of the users, you are passing an empty cards array on which you are performing map operation which not surprisingly returns an empty array itself, hence you are not getting any state changes.
The logic of passing the setCards is correct.
Here is a small example where state changes are taking place and also showing.
import React, { useState } from "react";
const App = () => {
const [cards, setCards] = useState([]);
return (
<>
<Card
setCards={setCards}
cards={cards}
/>
<p>{cards.toString()}</p>
</>
);
};
const Card = ({ setCards, cards }) => {
const addOption = () => {
setCards(["1","2"]);
};
return (
<>
<button onClick={addOption} type="button">
Add Option
</button>
</>
);
};
export default App;
Screenshot:
Codesandbox Link