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.
Related
In a form that I am making the material that is being created in the form should have multiple width options that can be added. This means that I will have a text input where the user can add an option, and when this option is added, it should be added to the React Hook Form widthOptions array, without using the regular react state. How would one do this? How do you add an item to the total React Hook Form state, I only see options for just one input field corresponding to a property.
This is how i would do it using the regular React state
import { TrashIcon } from "#heroicons/react/24/outline";
import React, { useRef, useState } from "react";
const Test = () => {
const [widthOptions, setWidthOptions] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const removeWidthOption = (widthOption: string) => {
setWidthOptions(widthOptions.filter((option) => option !== widthOption));
};
const addWidthOption = (widthOption: string) => {
setWidthOptions([...widthOptions, widthOption]);
};
const editWidthOptions = (widthOption: string, index: number) => {
const newWidthOptions = [...widthOptions];
newWidthOptions[index] = widthOption;
setWidthOptions(newWidthOptions);
};
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={() => addWidthOption(inputRef?.current?.value)}>
Add Width Option
</button>
{widthOptions.map((option, index) => (
<div className="flex">
<input
type="text"
value={option}
onChange={() => editWidthOptions(option, index)}
/>
<button type="button" onClick={() => removeWidthOption(option)}>
<TrashIcon className="w-5 h-5 mb-3 text-gray-500" />
</button>
</div>
))}
</div>
);
};
export default Test;
You can just the controller component for this as for all other fields.
Since you have not shared any of you code here is a generic multi-select
<Controller
name={name}
render={({ field: { value, onChange, ref } }) => {
return (
// You can use whatever component you want here, the you get the value from the form and use onChange to update the value as you would with a regular state
<Test
widthOptions={value}
setWidthOptions={onChange}
/>
);
}}
/>;
https://react-hook-form.com/api/usecontroller/controller/
And in you Test component remove the state and get the props instead
const Test = ({widthOptions, setWidthOptions}) => {
const inputRef = useRef<HTMLInputElement>(null);
.
.
.
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} />;
}
This is working perfectly as a single component. I am trying to remove the search function because I render the component in another component which is embedded in a list. I don't want the search to appear in multiple places. I want to separate it and put it in a parent component.
I don't know how to handle this. I have tried to use props, probably I am doing it the wrong way.
import React, {useState} from 'react'
function TagsInput(props) {
const [tags, setTags] = useState([])
const [search, setSearch] = useState("");
const addTags = event => {
if (event.key === "Enter" && event.target.value !== "") {
setTags([...tags, event.target.value]);
props.selectedTags([...tags, event.target.value]);
event.target.value = "";
}
};
const removeTags = index => {
setTags([...tags.filter(tag => tags.indexOf(tag) !== index)]);
};
const handleFilterChange = e => {
setSearch(e.target.value)
}
function DataSearch(rows) {
return rows.filter((row) => row.toLowerCase().indexOf(search.toLowerCase()) > -1);
}
const searchPosts = DataSearch(tags);
return (
<>
<div>
<input value={search} onChange={handleFilterChange} placeholder={"Search"} />
</div>
<div className="tags-input">
<ul>
{searchPosts.map((tag, index) => (
<li key={index}>
<span>{tag}</span>
<i
className="material-icons"
onClick={() => removeTags(index)}
>x</i>
</li>
))}
</ul>
<input
type="text"
onKeyUp={event => addTags(event)}
placeholder="Press enter to add tags" />
</div>
</>
)
}
export default TagsInput
I am using this component in another component. I want to remove the search input but I don't know how to do it as it is component of a function not defined.
<div>
<input value={search} onChange={handleFilterChange} placeholder={"Search"} />
</div>
In the parent component
<TagsInput /> is embedded in it map function and I want avoid the search being created in multiple li.
Thank you and I am sorry for dump question as I am new to it.
What exactly do you need from this component? If you just need to remove the search input box you can just remove
<div>
<input
value={search}
onChange={handleFilterChange}
placeholder={'Search'}
/>
</div>
It seems you are calling this component inside a parent component loop. If you just need to call this component once you have to use it outside the loop.
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'm creating a project-planning app using React, Redux, and Firebase. A single project record in my Firestore database contains a Title and some Content. When I go to update a project, I have the input fields' defaultValues set to the correct data for the project I want to edit. However, updating only works if I make changes to both the Content and Title input fields. Otherwise, upon submitting these values the data gets deleted because the local state has not seen any changes and therefore updates the untouched field to the empty string: ""
I have tried setting the local state of the EditProject component in the render method, but this is not possible:
render() {
const { project, auth } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
if (project) {
this.setState({
title: project.title,
content: project.content
});
...
I have also tried setting the state in during componentDidMount like so:
componentDidMount = () =>{
const { project } = this.props;
this.setState({
title: project.title,
content: project.content
})
}
But the issue with this is that the project prop does not get mapped by mapStateToProps before componentDidMount
Lastly, I've tried passing the project prop from the parent component, which is projectDetails, but I am unable to successfully do so. I might be doing this part wrong so please let me know if there is a good way to do this with the code I have. In ProjectDetails:
<Link to={"/edit/" + docId} key={docId}>
<button className="btn pink lighten-1 z-depth-0">Edit</button>
</Link>
This links to the 'broken' EditDetails component I am trying to fix.
Here is my code for the EditProject component
class EditProject extends Component {
state = {
title: "",
content: ""
};
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
handleSubmit = e => {
e.preventDefault();
let localProject = this.state;
let docId = this.props.docId;
this.props.editProject(localProject, docId);
const projectDetailURL = "/project/" + docId;
this.props.history.push(projectDetailURL);
};
render() {
const { project, auth } = this.props;
if (!auth.uid) return <Redirect to="/signin" />;
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
<div className="card-content">
<form onSubmit={this.handleSubmit} className="white">
<h5 className="grey-text text-darken-3">Edit Project</h5>
<div className="input-field">
<label htmlFor="title" className="active">
Title
</label>
<input
onChange={this.handleChange}
type="text"
id="title"
defaultValue={project.title}
/>
</div>
<div className="input-field">
<label htmlFor="content" className="active">
Edit Project Content
</label>
<textarea
id="content"
onChange={this.handleChange}
className="materialize-textarea"
defaultValue={project.content}
/>
</div>
<div className="input-field">
<button className="btn pink lighten-1 z-depth-0">
Update
</button>
</div>
</form>
</div>
<div className="card-action grey lighten-4 grey-text">
<div>
Posted by {project.authorFirstName} {project.authorLastName}
</div>
<div>{moment(project.createdAt.toDate()).calendar()}</div>
<div className="right-align" />
</div>
</div>
</div>
);
} else {
return (
<div className="container center">
<p>Loading project...</p>
</div>
);
}
}
}
const mapStateToProps = (state, ownProps) => {
//id = the document id of the project
const id = ownProps.match.params.id;
const projects = state.firestore.data.projects;
const project = projects ? projects[id] : null;
return {
project: project,
auth: state.firebase.auth,
docId: id
};
};
const mapDispatchToProps = dispatch => {
return {
editProject: (project, docId) => dispatch(editProject(project, docId))
};
};
export default compose(
connect(
mapStateToProps,
mapDispatchToProps
),
firestoreConnect([
{
collection: "projects"
}
])
)(EditProject);
Upon visiting the edit page, I would like the data to remain unchanged if a user does not make any changes to an input field.
I was able to properly update my local state by using React Router to pass props to my EditProject component from its "parent component". I used the React router to do this since the EditProject component is not actually nested inside this "parent component".
Here's how you can pass props to other components using React Router:
Specify where you want to send your props and what you want to send:
//ProjectDetails Component
<Link to={{
pathname: "/edit/" + docId,
state: {
title: project.title,
content: project.content
}
}}>
<button className="btn">Edit</button>
</Link>
Aquire props in the componentDidMount() lifecycle method and update the local state using setState().
//EditProject Component (component recieving props from ProjectDetails)
class EditProject extends Component {
state = {
title: "",
content: ""
};
componentDidMount = () => {
//Aquire proprs from React Router
const title = this.props.location.state.title
const content = this.props.location.state.content
//Update the local state
this.setState({
title: title,
content: content
})
}
I hope this helps!