I have Form component which should get form elements like inputs, buttons etc. How i can detect onChange or onClick events or value if they passed like children? For example i change the second input and how detect that i change exactly second input but not another
I need to detect all actions in Form component
const Form = ({children, ...rest}) => {
const onChangeHandler = () => {
//detect change input
}
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{children}
</div>
)
}
const App = () => {
return <Form>
<input style={{width: '100%'}}/>
<input style={{width: '100%'}}/>
<input style={{width: '100%'}}/>
</Form>
}
ReactDOM.render(<App/>, document.getElementById('root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"/>
How about using the React.cloneElement method?
const Form = ({children, ...rest}) => {
const onChangeHandler = (e) => {
const value = e.target.value
const id = e.target.id
console.log(value, id)
}
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
onChange: onChangeHandler
})
})}
</div>
)
}
const App = () => {
return <Form>
<input id="input1" />
<input id="input2" />
<input id="input3" />
</Form>
}
ReactDOM.render(<App/>, document.getElementById('root'))
Update: yes sorry it was untested, there's no way the input would know about the Form onChangeHandler so you just map it to the onChange in cloneElement.
I've added id attributes to each input so that you can see how the value for each input is change in the onChangeHandler. If you are wanting to save the value to state you can then use this id as a key in something like Redux.
React.cloneElement is what you need for this.
From the docs,
Clone and return a new React element using element as the starting
point. The resulting element will have the original element’s props
with the new props merged in shallowly. New children will replace
existing children. key and ref from the original element will be
preserved.
import React from 'react'
import ReactDOM from 'react-dom'
const Form = ({children, ...rest}) => {
const onChangeHandler = e => {
//detect change input
const value = e.target.value
const name = e.target.name
console.log('You have changed ', name, ' with value ', value)
}
return (
<div style={{display: 'flex', flexWrap: 'wrap'}}>
{React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
onChange: onChangeHandler,
name: `input${index + 1}`,
})
})}
</div>
)
}
const App = () => {
return (
<Form>
<input />
<input />
<input />
</Form>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
Demo
Related
I'm new to nextjs/react so bare with me here. In my project I have multiple select elements with multiple options in each one. When an option is selected or changed I want to pass that value to another component. I was able to pass an onClick event to another component but when I tried a similar solution I wasn't able to get it to work. So the select elements are being mapped in Component A2, but the options for those elements are also being mapped in Component A3 and I need to pass the value to Component B2. You will see in my code I tried to pass it with the "handleOnChange". I'm not very good at explaining things so here is my code snippets, I hope this makes sense:
Parent Component
export default function Post({ globalProps, page, globalPages, sidebarProps }) {
const [addFlexItem, setAddFlexItem] = useState(false)
const [addFlexItemStyles, setFlexItemStyles] = useState()
return (
<Layout globalProps={globalProps}>
<main className={styles.container}>
<FlexSidebar sidebarProps={sidebarProps} onClick={() => setAddFlexItem(true)} handleOnChange={() => setFlexItemStyles()} />
<FlexContainer addFlexItem={addFlexItem} addFlexItemStyles={addFlexItemStyles} />
</main>
</Layout>
)
}
Component A1
const FlexSidebar = ({ sidebarProps, onClick, handleOnChange }) => {
return (
<aside className={styles.left_sidebar}>
<section className={styles.wrap}>
{/* we are padding the onClick to the child component */}
{container === true && <FlexSidebarContainer sidebarProps={sidebarProps} onClick={onClick} handleOnChange={handleOnChange} />}
{items === true && <FlexSidebarItems sidebarProps={sidebarProps} />}
</section>
</aside>
)
}
Component A2
const FlexSidebarContainer = ({ sidebarProps, onClick, handleOnChange }) => {
const options = sidebarProps.options
return (
<>
<p className={styles.warning}>{sidebarProps.containerWarningText}</p>
<button type="button" className="btn" onClick={() => onClick()}>
{sidebarProps.addItemBtn}
</button>
<form className={styles.form}>
{options.map((option, index) => {
return (
<div key={index} className={styles.form_item}>
<div className={styles.form_label_wrap}>
<label>{option.title}</label>
</div>
<FlexSidebarSelect options={option.items} handleOnChange={handleOnChange} />
</div>
);
})}
</form>
</>
)
}
Component A3
const FlexSidebarSelect = ({ options, handleOnChange }) => {
return (
<div className={styles.form_item_wrap}>
<select onChange={(value) => handleOnChange(value)}>
{options.map((item, index) => {
return (
<option key={index} value={item.value}>{item.item}</option>
)
})}
</select>
</div>
)
}
Component B1
const FlexContainer = ({ addFlexItem, addFlexItemStyles }) => {
return (
<section className={styles.right_content}>
<FlexItem addFlexItem={addFlexItem} addFlexItemStyles={addFlexItemStyles} />
</section>
)
}
Component B2
const FlexItem = ({ addFlexItem, addFlexItemStyles }) => {
const [isaddFlexItem, setaddFlexItem] = useState(addFlexItem)
useEffect(() => {
setaddFlexItem(addFlexItem)
}, [addFlexItem])
return (
isaddFlexItem ?
<div className={styles.flex_item}>
<div className={styles.flex_item_wrap}>
<div className={styles.flex_item_inner}>
</div>
<button className={styles.trash}>
</button>
</div>
</div>
: "empty"
)
}
I will add that if I change the code in Component A3 to this, im able to log the value, but I cant get it to work in the parent component.
const FlexSidebarSelect = ({ options, handleOnChange }) => {
const [value, setValue] = useState("")
const handleOptionChange = (e) => {
let value = e.target.value
setValue({
value
})
}
return (
<div className={styles.form_item_wrap}>
<select onChange={handleOptionChange}>
{options.map((item, index) => {
return (
<option key={index} value={item.value}>{item.item}</option>
)
})}
</select>
</div>
)
}
I'm building a controlled form with dynamic fields.
The Parent component get data from a redux store and then set state with the values.
I don't want to make it with too much code lines so I turn the dynamic fields into a component.
States stay in the parent component and I use props to pass the handlechange function.
Parent :
function EditAbout(props) {
const [img, setImg] = useState("");
const [body, setBody] = useState(props.about.body);
const [instagram, setInstagram] = useState(props.about.links.instagram);
const [linkedin, setLinkedIn] = useState(props.about.links.linkedin);
const [press, setPress] = useState(props.about.press)
const handleSubmit = (e) => {
// Submit the change to redux
};
// set states with redux store
useEffect(() => {
setBody(props.about.body);
setInstagram(props.about.links.instagram);
setLinkedIn(props.about.links.linkedin);
setPress(props.about.press);
}, []);
const handleChangeChild = (e, index) => {
e.preventDefault();
let articles = press
const {value, name } = e.target
if (name === "title") {
articles[index].title = value;
} else {
articles[index].link = value;
}
setPress(articles)
console.log(articles[index])
}
return (
<Box>
<h1>CHANGE ABOUT ME</h1>
<Input
label="Image"
name="img"
type="file"
variant="outlined"
margin="normal"
onChange={(e) => setImg(e.target.files)}
/>
<Input
label="body"
value={body}
name="body"
onChange={(e) => setBody(e.target.value)}
variant="outlined"
multiline
rowsMax={12}
margin="normal"
/>
<Input
label="instagram"
value={instagram}
name="instagram"
variant="outlined"
margin="normal"
onChange={(e) => setInstagram(e.target.value)}
/>
<Input
label="Linkedin"
value={linkedin}
name="linkedin"
variant="outlined"
margin="normal"
onChange={(e) => setLinkedIn(e.target.value)}
/>
<Child press={press} onChange={handleChangeChild} />
{props.loading ? (
<CircularProgress color="black" />
) : (
<Button onClick={handleSubmit} variant="contained">
Send
</Button>
)}
</Box>
);
}
Child :
function Child(props) {
const { press, onChange } = props;
const inputsMarkup = () =>
press.map((article, index) => (
<div key={`press${index}`} style={{ display: "flex" }}>
<input
name="title"
value={press[index].title}
onChange={(e) => onChange(e, index)}
/>
<input
name="link"
value={press[index].link}
onChange={(e) => onChange(e, index)}
/>
<button>Delete</button>
</div>
));
return (
<div>
<h1>Press :</h1>
{inputsMarkup()}
</div>
);
}
Everything is fine when I'm typing in the Parent inputs. But when I'm using Child fields state update for one character but come back at its previous state right after.
It also doesn't display the character change. I can only see it in the console.
Thanks you in advance for your help
The problem is that you're mutating the state directly. When you create the articles variable (let articles = press) you don't actually create a copy and articles doesn't actually contain the value. It's only a reference to that value, which points to the object’s location in memory.
So when you update articles[index].title in your handleChangeChild function, you're actually changing the press state too. You might think that's fine, but without calling setPress() React will not be aware of the change. So, although the state value is changed, you won't see it because React won't re-render it.
You need to create a copy of the press array using .map() and create a copy of the updated array element. You can find the updated handleChangeChild() below:
const handleChangeChild = (e, index) => {
e.preventDefault();
const { value, name } = e.target;
setPress(
// .map() returns a new array
press.map((item, i) => {
// if the current item is not the one we need to update, just return it
if (i !== index) {
return item;
}
// create a new object by copying the item
const updatedItem = {
...item,
};
// we can safely update the properties now it won't affect the state
if (name === 'title') {
updatedItem.title = value;
} else {
updatedItem.link = value;
}
return updatedItem;
}),
);
};
I'm trying to create a function that renders an array of links and i want to create a text input and a button that adds value from input in the array. I got the links saved in the state in the object that looks like this:
sourceLinks: {
0: "https://www.w3schools.com/html/"
1: "https://www.apachefriends.org/docs/"
2: "https://docs.moodle.org/38/en/Windows_installation_using_XAMPP"
}
I've managed to render the links like this:
renderLinks() {
let sessionLinks = this.state.sessionLinks;
let links = [];
Object.values(sessionLinks).map((link) => {
links.push(<div className="column">
<span>
<InputPreview inputValue={link} classes="w-300" />
</span>
</div>)
})
return links;
}
InputPreview is the component i use for displaying links. I'm tryin to add a text input and a button bellow the rendered links that adds the value to the array, and an icon next to every link that removes it from an array. I'm trying to do it all in one function renderLinks() and then call it in render. I know i have to push and slice items from an array and update the state but i'm strugling cause i just started learning react. Please help :)
You can add and render links with below code.
import React from "react";
class ItemList extends React.Component {
state = {
links: ["item1"],
newItem: ""
};
submit(e, newLink) {
e.preventDefault();
let updatedLinks = this.state.links;
updatedLinks.push(newLink);
this.setState({ links: updatedLinks });
}
render() {
return (
<React.Fragment>
<ul>
{this.state.links?.map((link, i) => (
<li key={i}>
<p>{link}</p>
</li>
))}
</ul>
<form onSubmit={(e) => this.submit(e, this.state.newItem)}>
<input
type="text"
value={this.state.newItem}
onChange={(e) => this.setState({ newItem: e.target.value })}
/>
<button type="submit">ADD</button>
</form>
</React.Fragment>
);
}
}
export default ItemList;
Let me know for further clarificaton.
This is a example with functional components and hooks
import React, { useState } from 'react';
const sourceLinks = [
'https://www.w3schools.com/html/',
'https://www.apachefriends.org/docs/',
'https://docs.moodle.org/38/en/Windows_installation_using_XAMPP',
];
export const ListLinks = () => {
const [links, setLinks] = useState(sourceLinks);
const [newLink, setNewLink] = useState('');
const handleAdd = () => {
setLinks(links => [...links, newLink]);
};
const handleChangeNewLink = e => {
const { value } = e.target;
setNewLink(value);
};
return (
<div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<input type='text' value={newLink} onChange={handleChangeNewLink} />
<button onClick={handleAdd}>Add</button>
</div>
<br />
{links.map((link, index) => (
<p key={index}>{link}</p>
))}
</div>
);
};
This is the result:
Lastly, read the documentation, managing the state is essential.
I'm creating a webapp that allows users to create custom input components. This is in the form of MUI Select and TextFields:
const CTextField = (
<TextField id="outlined-basic" variant="outlined" size="small" />
);
const CSelect = function(match) {
match = match.substring(1, match.length - 1);
const matches = match.split("/");
let menus = [];
matches.forEach(single => {
menus.push(<MenuItem value={single}>{single}</MenuItem>);
});
return (
<FormControl>
<Select>{menus}</Select>
</FormControl>
);
};
The issue I'm coming across is, how can I grab all the values placed or selected from the components. They're created by the end user, so there can be any number of them, and I'm trying to keep it in order with plain text.
What I think works is simply grabbing the raw data including html and stripping it away, then grabbing the values. But because I want this to be dynamic, that'd end up being slow the larger the text is. Is there an efficient method of grabbing all the raw text as well as the values of the input components?
Don't worry about how many elements you have - that's why Arrays are there:
const { useState } = React
const App = (props) => {
const [inputFieldsList, setInputFieldsList] = useState([]);
return (
<div>
<h1>Input field list:</h1>
<button
onClick={() => {
const newVal = [...inputFieldsList, '']
setInputFieldsList(newVal)
}}>ADD INPUT FIELD</button>
{inputFieldsList.map((item, i) => (
<div>
<input
type="text"
onChange={(e) => {
const newList = [...inputFieldsList]
newList[i] = e.target.value
setInputFieldsList(newList)
console.log(inputFieldsList)
}}
/>
</div>
))}
</div>
);
}
const rootElement = document.getElementById('app')
ReactDOM.render(<App />, rootElement)
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You should use useState for storing the values of the input, and onChange function to set your state when a user types in the input.
const Hello = () => {
const[values, setValues] = useState({
input1: '',
input2: ''
});
const changeHandler = () => {
setValues(
...values,
[event.target.name]: event.target.value
);
}
return (
<>
<input type="text" value={values.input1} name="input1" onChange={changeHandler} />
<input type="text" value={values.input2} name="input2" onChange={changeHandler} />
</>
);
}
What's the best way to store values typed into the text fields here?
const AddUserPage = () => (
<div>
<PermanentDrawerLeft></PermanentDrawerLeft>
<div className='main-content'>
<form className="ROOT" noValidate autoComplete="off">
<TextField id="standard-basic" label="Standard" />
</form>
</div>
</div>
);
export default AddUserPage;
I want to find a way such that I can use the stored values in my GraphQL mutations as well, without having to modify the const() structure of my page. I don't want to use the Class Component Extend or function structure here.
What is your const() structuremakes:
=> (This is the auto return syntax.)
If you want to store/reuse your value, you will have to define some state/variable to store the data.
You can also do it in upper component like:
import React, { useState } from "react";
const Parent = props => {
const [state, setState] = useState({ text: "" });
return <AddUserPage value={state.text} onChange={e => setState(prev => ({ ...prev, text: e.target.value || "" }))} />
}
const AddUserPage = ({ value = "" , onChange }) => (
<div>
<PermanentDrawerLeft></PermanentDrawerLeft>
<div className='main-content'>
<form className="ROOT" noValidate autoComplete="off">
<TextField id="standard-basic" value={value} onChange={onChange} label="Standard" />
// value, and Onchange comes from an upper component
</form>
</div>
</div>
);