I want to set up a custom Form component using react-hook-form that can handle fields that are potentially nested or wrapped in other elements. My approach is to go through the component tree and recursively pass register and errors (returned by useForm()) to all leaf nodes that are input fields.
For example, a simple sign-up form. The first and last name fields are wrapped in a div that styles them to be on the same line. The form looks like this:
<Form
onSubmit={onSubmit}
styles={["login_form"]}
showButton
buttonText='SIGN UP'>
// These are the wrapped fields
<div className='signup_name_field'>
<TextInput name={"fname"} label={"first"} />
<TextInput name={"lname"} label={"last"} />
</div>
<TextInput name={"email"} label={"email"} />
<TextInput password={true} name={"password"} label={"password"} />
<TextInput
password={true}
name={"passwordConfirm"}
label={"confirm password"}
/>
</Form>
I created a custom Form component following react-hook-form's example but added a recursive function to handle nested or wrapped fields:
const recursiveInjectProps: any = (children: any) => {
return React.Children.map(children, (child) => {
if (child.props.children) {
recursiveInjectProps(child.props.children);
return child;
} else {
if (child.props.name) {
return React.createElement(child.type, {
...{
...child.props,
register: register,
key: child.props.name,
},
});
} else {
return child;
}
}
});
};
return (
<form className={styles.join(" ")} onSubmit={handleSubmit(onSubmit)}>
{recursiveInjectProps(children)}
{renderButton()}
</form>
);
And TextInput looks like this:
const checkRegister = () => {
if (register) {
return (
<div className='login_field'>
<input
className='login_input'
name={name}
placeholder={label}
ref={register(validation)}
type={password ? "password" : "text"}
{...rest}
/>
<label htmlFor={name} className='login_label' />
{errors && errors[name] && errors[name].message}
</div>
);
} else {
return <div>no dice</div>;
}
};
return checkRegister();
The issue is that recursiveInjectProps() is failing to inject register and errors for children that are more than one layer deep (so, the name fields that are wrapped in the div).
I know this because when it renders I see "no dice" where the name fields should be.
Would really appreciate any help with this.
Perhaps you should use context API: https://react-hook-form.com/api#useFormContext
import React from "react";
import { useForm, FormProvider, useFormContext } from "react-hook-form";
export default function App() {
const methods = useForm();
const onSubmit = data => console.log(data);
return (
<FormProvider {...methods} > // pass all methods into the context
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NestedInput />
<input type="submit" />
</form>
</FormProvider>
);
}
function NestedInput() {
const { register } = useFormContext(); // retrieve all hook methods
return <input name="test" ref={register} />;
}
Related
I am trying to pass the name, bio, experience, ctc props from InputForUserProfile to the userprofile.So that I can display them in my userprofile when ever any changes happen. But I can't pass them and itsays undefined when I logged them in console.Note(userprofile.js is a custom web component I created)These are the codes I tried.
App.js:
import "./App.css";
import Routing from "./Routing";
function App() {
return (
<div className="App">
<Routing/>
</div>
);
}
export default App;
Routing.js:
import InputForUserProfile from "./InputForUserProfile";
import "./userprofile.js";
import { Link, Route, Routes } from "react-router-dom";
const Routing = ({name, bio, experience, ctc}) => {
console.log("1", name);
return (
<>
<nav>
<ul>
<li>
<Link to="*">InputFields</Link>
</li>
<li>
<Link to="/userprofile">userprofile</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="*" element={<InputForUserProfile />} />
<Route
path="/userprofile"
element={
<user-profile
name={name}
bio={bio}
exp={experience}
ctc={ctc}
/>
}
/>
</Routes>
</>
);
};
export default Routing;
InputForUserProfile.js:
import { useState } from "react";
import { Route, Routes } from "react-router-dom";
import "./userprofile.js";
const InputForUserProfile = () => {
const [name, setName] = useState(localStorage.getItem("name") || "");
const [bio, setBio] = useState(localStorage.getItem("bio") || "");
const [experience, setExperience] = useState(
localStorage.getItem("experience") || ""
);
const [ctc, setCtc] = useState(localStorage.getItem("ctc") || "");
const handleSubmit = (event) => {
event.preventDefault();
localStorage.setItem("name", name);
localStorage.setItem("bio", bio);
localStorage.setItem("experience", experience);
localStorage.setItem("ctc", ctc);
};
return (
<>
<form onSubmit={handleSubmit}>
<label>
Change User Name:
<input
type="text"
placeholder="Change User Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<br />
<label>
Change User Bio:
<textarea
placeholder="Change User Bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
</label>
<br />
<label>
Change User Experience:
<input
type="text"
placeholder="Change User Experience"
value={experience}
onChange={(e) => setExperience(e.target.value)}
/>
</label>
<br />
<label>
Change User CTC:
<input
type="text"
placeholder="Change User CTC"
value={ctc}
onChange={(e) => setCtc(e.target.value)}
/>
</label>
<br />
<button type="submit">Save Changes</button>
</form>
<Routes>
<Route
path="/userprofile"
element={
<user-profile name={name} bio={bio} exp={experience} ctc={ctc} />
}
/>
</Routes>
</>
);
};
export default InputForUserProfile;
userprofile.js(custom web component):
class UserProfile extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<div id="profile">
<br /><br />
<img
src=""
alt="Profile Picture"
/>
<h1>
Name:
<p id="name"></p>
</h1>
<h1>
BIO:
<p id="bio"></p>
</h1>
<h1>
Experiance:
<p id="exp"></p>
</h1>
<h1>
CTC:
<p id="CTC"></p>
</h1>
<input type="text" id="user-name" class="hide-input" placeholder="changeusername">
<input type="text" id="user-bio" class="hide-input" placeholder="changeuserbio">
<input type="text" id="user-experience" class="hide-input" placeholder="changeuserexperience">
<input type="text" id="user-CTC" class="hide-input" placeholder="changeuserCTC">
<button id="save-button" class="hide-input" >save</button>
<button id="edit-button" >Edit Profile</button
><br /><br />
</div>`;
}
connectedCallback() {
const userVaule = this.shadowRoot.querySelector("div");
this.shadowRoot.querySelector('#name').textContent = this.getAttribute("name");
this.shadowRoot.querySelector('#bio').textContent = this.getAttribute("bio");
this.shadowRoot.querySelector('#exp').textContent = this.getAttribute("exp");
this.shadowRoot.querySelector('#CTC').textContent = this.getAttribute("ctc");
userVaule
.querySelector("#save-button")
.addEventListener("click", this.saveProfile.bind(this));
userVaule
.querySelector("#edit-button")
.addEventListener("click", this.editProfile.bind(this));
userVaule.querySelectorAll("input, #save-button").forEach((el) => {
el.classList.add("hide-input");
});
}
editProfile() {
this.shadowRoot.querySelectorAll("input, #save-button").forEach((el) => {
el.classList.remove("hide-input");
});
this.shadowRoot.querySelector("#user-name").value =
localStorage.getItem("name") || "";
this.shadowRoot.querySelector("#user-bio").value =
localStorage.getItem("bio") || "";
this.shadowRoot.querySelector("#user-experience").value =
localStorage.getItem("experience") || "";
this.shadowRoot.querySelector("#user-CTC").value =
localStorage.getItem("ctc") || "";
}
saveProfile() {
this.shadowRoot.querySelectorAll("input, #save-button").forEach((el) => {
el.classList.add("hide-input");
});
let name = this.shadowRoot.querySelector("#name");
let bio = this.shadowRoot.querySelector("#bio");
let exp = this.shadowRoot.querySelector("#exp");
let CTC = this.shadowRoot.querySelector("#CTC");
const userName = this.shadowRoot.querySelector("#user-name").value;
localStorage.setItem("name", userName);
const userBio = this.shadowRoot.querySelector("#user-bio").value;
localStorage.setItem("bio", userBio);
const userExperience =
this.shadowRoot.querySelector("#user-experience").value;
localStorage.setItem("exp", userExperience);
const userCTC = this.shadowRoot.querySelector("#user-CTC").value;
localStorage.setItem("CTC", userCTC);
name.textContent = userName;
bio.textContent = userBio;
exp.textContent = userExperience;
CTC.textContent = userCTC;
}
}
customElements.define("user-profile", UserProfile);
There are two problems with your code - one for now and another in future.
First is properties vs attributes. React doesn't support passing props (non-primitive types like objects, array, etc.) to the custom elements. Only values like strings, numbers are supported and they are passed as attributes instead of DOM properties. (But this doesn't look to be the issue right now as all the data is just strings).
The way to get around is to wrap your web component in some react component and use the hook useRef to get instance of actual web component and when the props changes, you change the prop of the web component like:
function MyWrapperComp(props) {
const ref = useRef(null);
// Observe
useEffect(() => {
// Set the name as prop as opposed to attribute
ref.current?.name = props.name;
}, props.name);
return (
<user-profile ref={ref}></user-profile>
);
}
If you are fine with using just attribute (as opposed to props), then the second issue is the design of your web component. It may happen that your component is initialized but react has not yet passed the props to the component (async behavior or something similar - maybe localstorage empty initially). To get around that, you should listen for attributeChangedCallback lifecycle event or use MutationObserver to observe attribute changes done by React. For example:
class UserProfile extends HTMLElement {
// Rest of the code.
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'name' && oldValue !== newValue) {
// Write code here when attribute `name` is changed.
}
}
}
Finally to answer your original question - How to pass prop from one component to another component - unless you are using any declaration abstraction on top of web components, the only way is to grab the instance of the component and pass props to it using that instance.
On a side note, writing web components without any abstraction is cumbersome and extremely error prone. I would recommend something like lit-element to author web components it provide many utilities to ease the pain.
I am trying to figure how to update an array state and output the changes in input for my React Js Project.
I am trying to display multiple input components on the screen by mapping an array with the input component as it's child. I want to change the value of individual item with onChange()function and want to view the changes inside the input component.
import { useState } from "react";
import "./styles.css";
export default function App() {
const [array, setArray] = useState(["1", "2", "3"]);
return (
<div className="App">
{array.map((item, index) => (
<input
type="text"
value={array[index]}
onInput={(e) => {
setArray((array) => {
array[index] = e.target.value;
return array;
});
}}
/>
))}
</div>
);
}
The Updates are not being displayed.
This should work :
<input
key={index}
type="text"
value={array[index]}
onInput={(e) => {
setArray((prevArr) => {
const result = [...prevArr];
result[index] = e.target.value;
return result;
});
}}
/>
I am missing how to bring the input value out of the child component.
I have an Input component that I want to re-use, there I can see what happens onChange, and I see every change on the input field.
Then on the parent component, Form, where I use <Input />, I have the rest of the form with the Submit button. At this level, I want to handle onSubmit, but I cannot see/console the value of the input here. I can only see it from the child.
Any ideas about what I am doing wrong?
Input.js - here I can see the input value onChange
function Input(props) {
const { label, name, value } = props;
const handleChange = (event) => {
const updateForm = {...Form};
console.log("change:", updateForm)
updateForm[label] = event.target.value;
}
return (
<label>
{label}
<input name={name} value={value} onChange={handleChange}></input>
</label>
)
}
export { Input }
Forms.js - here I cannot get access to the input value and submit/handle it
function Form(props) {
const handleSubmit = (event) => {
event.preventDefault();
console.log(Input.value);
console.log(props.label.value)
alert(`form is: ${event.target.input}`);
}
return (
<>
<form onSubmit={handleSubmit}>
<Input label={props.label} />
<input type="submit" value="Submit"></input>
</form>
</>
)
}
I have that structure because I am defining what I want in my Form on the main HomePage component:
function Home() {
return (
<>
.......
<Section withForm label={["Name"]} {...homeObjFive}/>
<Section withForm label={"Phone"} {...homeObjOne}/>
.......
</>
)
}
This is the perfect case to use the useRef function from react.
In Form.js
import React, { useRef } from 'react'
create a reference constant and pass it as a prop into the input component. and change the input value that is handled in the onSubmit function to the reference
Also in Form.js (changes are made to the submit function)
function Form(props) {
const { inputValue } = useRef(); // added
const handleSubmit = (event) => {
event.preventDefault();
console.log(inputValue); // changed
console.log(props.label.value)
alert(`form is: ${event.target.input}`);
}
return (
<>
<form onSubmit={handleSubmit}>
{/* added the inputValue prop to the input component */}
<Input label={props.label} inputValue={inputValue} />
<input type="submit" value="Submit"></input>
</form>
</>
)
}
and now inside of the Input component set the input elements reference to the inputValue prop. you will no longer need a onChange function as reacts useRef function is updated automatically
In Input.js
function Input(props) {
return (
<label>
{props.label}
<input name={props.name} value={props.value} ref={props.inputValue}></input>
</label>
)
}
export { Input }
Suppose you have a form with two inputs, name and email (these are the id props of the inputs). You can extract the form values like this:
const handleSubmit = (event) =>
{
event.preventDefault()
const data = new FormData(event.currentTarget)
const name = data.get('name')
const email = data.get('email')
// do something with the data
}
You can read more about FormData here.
The scenario is an unknown number of input boxes are created with their own send button. A user enters some value and with onclick this value and a URI associated with that input is sent to a function which concatenates the two values and opens in the browser. I have done this in plain JS and in Angular 9 but I cannot figure out how to do this in React. I am only five days in my React adventure and I suspect my approach is incorrect.
In Angular I would create a reference to the input box and in my onclick I would add reference.value. How can I do that in React?
This is most likely a duplicate but I've been unable to find a QA that fits my use case.
Stackblitz starter app, right now it just passes a string.
class App extends React.Component {
constructor(props) {
super(props);
}
handleClick(data){
alert(data)
}
render() {
return (
<div>
<form>
<input placeholder='enter data' />
</form>
<br />
<botton className='btn' onClick={() => this.handleClick('test')}>Click</botton>
</div>
);
}
}
You can do so by using React.useRef:
import React from "react";
export default function App() {
const ref = React.useRef();
const handleClick = (data) => {
alert(data)
}
return (
<div>
<form>
<input ref={ref} placeholder='enter data' />
</form>
<br />
<botton className='btn' onClick={() => handleClick(ref.current.value)}>Click</botton>
</div>
);
}
See example on codesandbox.
Although, I would go with a different approach (get the value from the method and not from the call itself:
import React from "react";
export default function App() {
const ref = React.useRef();
const handleClick = () => {
if (ref) {
alert(ref.current.value);
}
};
return (
<div>
<form>
<input ref={ref} placeholder="enter data" />
</form>
<br />
<botton className="btn" onClick={handleClick}>
Click
</botton>
</div>
);
}
I am using redux-form But When I am start typing focus goes out first time in react.
In my component below, the input field loses focus after typing a character. While using Chrome's Inspector, it looks like the whole form is being re-rendered instead of just the value attribute of the input field when typing.
Please see below code:
<Field
name='description'
// onChange={this.handleChange.bind(this)}
//value={this.state.description}
component={props => {
return (
<MentionTextArea {...props} userTags={userTags} tags={postTags}/>
)
}}
MentionTextArea Component:
import React, {Component, PropTypes} from 'react'
import { MentionsInput, Mention } from 'react-mentions'
import defaultStyle from './defaultStyle'
class MentionTextArea extends Component {
constructor(props) {
super(prop)
}
handleOnChange (e) {
this.props.input.onChange(e.target.value);
}
render() {
// const { input, meta, ...rest } = this.props;
return (
<MentionsInput
value={this.props.input.value || ''}
onChange={this.handleOnChange.bind(this)}
singleLine={false}
style={ defaultStyle }
markup="#[__display__](__type__:__id__)"
>
<Mention trigger="#"
data={this.props.userTags}
type="userTags"
style={{ backgroundColor: '#d1c4e9' }}
renderSuggestion={ (suggestion, search, highlightedDisplay) => (
<div className="user">
{ highlightedDisplay }
</div>
)}
/>
<Mention trigger="#"
data={this.props.tags}
type="tags"
style={{ backgroundColor: '#d1c4e9' }}
renderSuggestion={ (suggestion, search, highlightedDisplay) => (
<div className="user">
{ highlightedDisplay }
</div>
)}
/>
</MentionsInput>
);
}
}
export default MentionTextArea
Please help!
Thanks in advance,
It's common problem for people new to redux-form please check this issue you'll find an answer there.
You must define the stateless function outside of your render() method, or else it will be recreated on every render and will force the Field to rerender because its component prop will be different. Example from official redux-form documentation:
// outside your render() method
const renderField = (field) => (
<div className="input-row">
<input {...field.input} type="text"/>
{field.meta.touched && field.meta.error &&
<span className="error">{field.meta.error}</span>}
</div>
)
// inside your render() method
<Field name="myField" component={renderField}/>