Access methods coming from props inside map function - javascript

I have the following two components App.js and PersonalInfo.js:
App.js
import React, { Component } from "react";
import "./styles.css";
import PersonalInfo from "./PersonalInfo";
class App extends Component {
state = [{ fname: "Jonny", lname: "Deep" }, { fname: "test", lname: "test" }];
inputChangeHandler = event => {
this.setState({
...this.state,
[event.target.name]: event.target.value
});
};
render() {
return (
<div className="App">
<PersonalInfo
data={this.state}
inputChangeHandler={this.inputChangeHandler}
/>
</div>
);
}
}
export default App;
PersonalInfo.js
import React from "react";
const PersonalInfo = props => {
const test = props.data.map((x, i) => {
return (
<div className="form-row align-items-center">
<div className="col-sm-3 my-1">
<label className="sr-only">First Name</label>
<input
type="text"
className="form-control"
id="inlineFormInputName"
name="fname"
value={x.fname}
onChange={props.inputChangeHandler}
/>
</div>
<div className="col-sm-3 my-1">
<label className="sr-only">Last Name</label>
<div className="input-group">
<input
type="text"
className="form-control"
id="inlineFormInputGroupUsername"
name="lname"
value={x.lname}
onChange={props.inputChangeHandler}
/>
</div>
</div>
<div className="col-auto my-1">
<button type="submit" className="btn btn-primary">
Add
</button>
<button type="submit" className="btn btn-danger">
Remove
</button>
</div>
</div>
);
});
return (
<div className="container">
<form>{test}</form>
<pre>{JSON.stringify(props, null, 2)}</pre>
</div>
);
};
export default PersonalInfo;
With the above code I'm getting error of props.data.map is not a function when I type in the input field. If I comment out or remove onChange={props.inputChangeHandler} it works. But onChange to update the state.
How do I remove the error and make it work ?
Here is the sandbox link: https://codesandbox.io/s/sharp-leakey-ub859?file=/src/App.js

Your inputChangeHandler is wrong, your updating an array using an object. So when you call it your array became an object like this { key: value }.
To achieve your goal you'll need a unique ID to easily find your data inside your array and then you need to modify this array :
state = [{ id: 1, fname: "Jonny", lname: "Deep" }, { id: 2, fname: "test", lname: "test" }]
inputChangeHandler = (event, id) => {
this.setState((prev) => {
const indexOfName = prev.findIndex(x => x.id === id);
prev[indexOfName][event.target.name] = event.target.value;
return prev;
})
}
And you need also to change the way you call it :
<input
type="text"
className="form-control"
id="inlineFormInputName"
name="fname"
value={x.fname}
onChange={(event) => props.inputChangeHandler(event, x.id)}
/>
I made a small working example using the index instead of id : https://codesandbox.io/embed/nifty-napier-ps86v?fontsize=14&hidenavigation=1&theme=dark

Related

How can I update the state of an object nested in an array from a child component?

I have RecipeCreate component, and inside of that I want a user to be able to render as many IngredientDetailsInput components as needed to complete the recipe.
I do this by creating an empty object in an ingredients array within RecipeCreate, and then iterate over this array of empty objects, generating a corresponding IngredientDetailsInput for each empty object.
From within IngredientDetailsInput I want to update the empty corresponding empty object in RecipeCreate with data passed up from IngredientDetailsInput. Since IngredientDetailsInput has the index of where it's object lives in the ingredients array in it's parent component, I believe this is possible.
Here is working sandbox that demonstrates the issue
I'm close, but each time the handleChange runs it is creating a new object in the ingredients array and I'm not sure why, or what other options to use besides handleChange - I'd like there not to have to be a form submit if possiblee
And here is code for both components
import React, { useState } from "react";
const RecipeCreate = (props) => {
const [ingredients, setIngredients] = useState([]);
const [recipeTitle, setRecipeTitle] = useState("");
//if an ingredient object has been added to the ingredients array
//render an IngredientDetailsInput component, passing along the index position
//so we can update it later
const renderIngredientComponents = () => {
if (ingredients) {
return ingredients.map((_, index) => {
return (
<IngredientDetailsInput
key={index}
position={index}
updateIngredientArray={updateIngredientArray}
/>
);
});
}
};
//broken function that should find the object position in ingredients
//and copy it, and non-mutated ingredient objects to a new object, and set the state to this
//new object
const updateIngredientArray = (key, value, position) => {
return setIngredients((prevIngredients) => {
console.log(ingredients)
return [...prevIngredients, prevIngredients[position][key] = value]
});
};
//allows the user to add another "ingredient", rendering a new IngredientDetailsInput component
//does so by adding a new, empty object to the ingredients array
const addElementToArray = () => {
setIngredients((prevIngredients) => [...prevIngredients, {}]);
};
return (
<div>
<div>
<form>
<div>
<label>Recipe Title</label>
<input
type="text"
name="recipeTitle"
value={recipeTitle}
onChange={(e) => setRecipeTitle(e.target.value)}
/>
</div>
<div>
<p>Ingredients</p>
{renderIngredientComponents()}
<div>
<p onClick={() => addElementToArray()}>+ ingredient</p>
</div>
</div>
<div></div>
<button type="submit">Submit</button>
</form>
</div>
</div>
);
};
export default RecipeCreate;
//child component that should allow changes to bubble up to RecipeCreate
export function IngredientDetailsInput(props) {
return (
<div>
<input
type="number"
name="measurement"
id="measurement"
placeholder="1.25"
onChange={(e) =>
props.updateIngredientArray(
"measurement",
e.target.value,
props.position
)
}
/>
<div>
<label htmlFor="measurementType">type</label>
<select
id="unitType"
name="unitType"
onChange={(e) =>
props.updateIngredientArray(
"unitType",
e.target.value,
props.position
)
}
>
<option>tbsp</option>
<option>cup</option>
<option>tspn</option>
<option>pinch</option>
<option>ml</option>
<option>g</option>
<option>whole</option>
</select>
</div>
<input
type="text"
name="ingredientName"
id="ingredientName"
placeholder="ingredient name"
onChange={(e) =>
props.updateIngredientArray(
"ingredientName",
e.target.value,
props.position
)
}
/>
</div>
);
}
The assignment prevIngredients[position][key] = value returns value instead of prevIngredients[position][key]. Thus when you setting the state, it returns the previous stored ingredients as well as that value.
const updateIngredientArray = (key, value, position) => {
return setIngredients((prevIngredients) => {
console.log(ingredients)
return [...prevIngredients, prevIngredients[position][key] = value]
});
};
A quick fix would be to recopy a new array of the current ingredient, then changing the position and key that you want.
const updateIngredientArray = (key, value, position) => {
const tmp = ingredients.map((l) => Object.assign({}, l));
tmp[position][key] = value;
setIngredients(tmp);
};
May be you can try like this?
const {useState} = React;
const App = () => {
const [state, setState] = useState([
{
name: "",
amount: "",
type: ""
}
]);
const addMore = () => {
setState([
...state,
{
name: "",
amount: "",
type: ""
}
]);
};
return (
<div className="App">
<h1>Recipe</h1>
<h2>Start editing to see some magic happen!</h2>
<label>Recipe Title</label>
<input type="text" />
<br /> <br />
<div onClick={addMore}>Add More +</div>
{state && state.map((val, ikey) =>
<div>
<br />
<label>Ingredients</label>
<input type="text" placeholder="Name" />
<input type="text" placeholder="Amount" />
<select>
<option>tbsp</option>
<option>cup</option>
<option>tspn</option>
<option>pinch</option>
<option>ml</option>
<option>g</option>
<option>whole</option>
</select>
</div>
)}
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

React Form: How to add error message that disappear if the input was typed in

I already built the form in React and it shows the input fields in red borders that'll change to regular borders once someone types it in. I used this example from this React form article link So everything is working except I wanted to add the error message under the input field that displays "Please fill in the blank field" that will disappear once someone starts typing in the field. How do I do this?
Here's my code in Form.js:
import React, { Component } from 'react';
import FormField from './FormFieldBox';
function validate(name, isin) {
// true means invalid, so our conditions got reversed
return {
name: name.length === 0,
isin: isin.length === 0
};
}
export default class PopupForm extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
isin: '',
country: '',
errormessage: ''
}
}
updateInput = (e) =>{
this.setState({[e.target.name]: e.target.value})
}
closePopupSubmit = (e) => {
if (!this.canBeSubmitted()) {
e.preventDefault();
}
let security = { //1.gather security data from form submit
name: this.state.name,
isin: this.state.isin,
country: this.state.country
}
this.props.submitPopup(security); //2.closePopup function, add security data
}
canBeSubmitted() {
const errors = validate(this.state.name, this.state.isin);
const isDisabled = Object.keys(errors).some(x => errors[x]);
return !isDisabled;
}
cancelPopupSubmit = (e) => {
e.preventDefault()
this.props.cancelPopup();
}
render() {
const errors = validate(this.state.name, this.state.isin);
const isDisabled = Object.keys(errors).some(x => errors[x]);
return (
<div className='popup'>
<div className='popup-inner'>
<form onSubmit={this.closePopupSubmit}>
<FormField onChange={this.updateInput} className={errors.name ? "input error" : "input"} label="Name" type="text" name="name" value={this.state.name} />
<FormField onChange={this.updateInput} className={errors.isin ? "input error" : "input"} label="ISIN" type="text" name="isin" value={this.state.isin} />
<FormField onChange={this.updateInput} label="Country" type="text" name="country" value={this.state.country} />
<button type="button" onClick={this.cancelPopupSubmit} className="button">Cancel</button>
<button type="submit" className="button" disabled={isDisabled}>Submit</button>
</form>
</div>
</div>
)
}
}
And my component FormField.js
import React from "react";
const FormBox = props => {
return (
<div className="field">
<label className="label">{props.label}</label>
<div className="control">
<input onChange={props.onChange}
className={props.className}
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder} />
{/* {props.errormessage} */}
</div>
</div>
)
}
export default FormBox;
const FormBox = props => {
return (
<div className="field">
<label className="label">{props.label}</label>
<div className="control">
<input onChange={props.onChange}
className={props.className}
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder} />
</div>
{Boolean(props.value.length) || (
<div className="err-msg">
Please fill in the blank field
</div>
)}
</div>
)
}
There are two ways you can achieve this
First : oninvalid attribute in HTML5 and calling a custom function on that.
Second : along with each element name object in state have a length attribute. In validation function you can check for the length and throw a custom error that you want to display.

add multiple input lines and add it to fetch method with reactJS

I want to add some input lines by a click of a button and add it to state so I can send it to the server, but I'm not sure how to add it to the fetch method or even if it's added to state,
this is what i have so far:
export class AdminPage extends React.Component {
constructor(props){
super(props);
this.state = {
sendeEmail: '',
matrialeliste: [{
matrialer: '',
antal: '',
pris: ''}]
};
}
handleUserInput = (e) => {
if (["matrialer", "antal", "pris"].includes(e.target.className) ) {
let matrialeliste = [...this.state.matrialeliste]
//matrialeliste[e.target.dataset.id][e.target.className] = e.target.value
this.setState({ matrialeliste }, () => console.log(this.state.matrialeliste))
} else {
const name = e.target.name;
const value = e.target.value;
this.setState({[name]: value};
}
}
addMatrialeliste = (e) => {
this.setState((prevState) => ({
matrialeliste: [...prevState.matrialeliste, {matrialer:"", antal:"", pris:""}],
}));
}
onSubmitSignIn = (event) => {
event.preventDefault();
fetch(`${api.url}/form`, {
method: 'post',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
sendeEmail: this.state.sendeEmail,
})
})
.then((response) => (response.json()))
.catch(error => console.log(error));
}
render(){
let {matrialeliste} = this.state;
return(
<div>
<div>
<h1>Arbejds seddel</h1>
<form>
<div>
<button type="button" onClick={this.addMatrialeliste}>
tilføj materialer
</button>
{
matrialeliste.map((val, idx) => {
return(
<div key={idx}>
<div>
<label htmlFor="matrialer">
Matrialeliste
</label>
<input name='matrialer' type="text" className='matrialer' onChange={this.handleUserInput} />
</div>
<div>
<label htmlFor="antal">
Antal
</label>
<input name='antal' type="number" className='antal' onChange={this.handleUserInput} />
</div>
<div>
<label htmlFor="pris">
Pris
</label>
<input name='pris' type="number" className='pris' onChange={this.handleUserInput} />
</div>
</div>)})}
<label htmlFor="email">
E-mail
</label>
<input name='email' type="email" onChange={e => this.handleUserInput} />
<button type="submit">Send som E-mail</button>
<div>
<button type="submit" disabled=this.state.formValid}>Create</button>
</div>
</div>
</form>
</div>
</div>
);
}
}
I can get to add extra lines, but I don't know how to add it to the fetch method.
I was thinking I could map it, but I'm still unsure how do that
Creating forms with plain React requires you to write each part of the process and for a complex state its become tough .so, my personal opinion is to go with Formik or React Hook Form as they cover most of the features.
In your case, I am assuming you want to sent whole state to fetch method .here is an example of your code which implemented with Formik library.

How to change DOM using onSubmit in React JS?

I am using react for the front end of a search application.
When user submits a query and a list of results pop up, each with a button that says "Learn More". When the "Learn More" button is pressed, the list of results should all disappear and be replaced with the information on that topic that was selected.
The search bar above should stay in place, if a user searches new information, the learn more info should go away and the new list of results should appear.
I am having trouble displaying the learn more information.
The biggest issue I am having is that I have to use the form with the onSubmit function and as soon as the onSubmit function is called my results will stay for a few seconds and then everything will disappear.
The following shows the parts of my file related to the issue
class Search extends React.Component {
learnMore(obj){
//Here is where i would like to replace the results class with the learn more info. obj.learnMore has the info stored
}
render() {
return (
<div className="search">
<div className="search-bar">
// Here is where my search bar is, results of search get added to results array
</div>
<div className= "results">
{this.state.results.map((obj) =>
<div key={obj.id}>
<p> {obj.name} </p>
<form id= "learn-more-form" onSubmit={() => {this.learnMore(obj); return false;}}>
<input type="submit" value="Learn More"/>
</form>
</div>
)}
</div>
</div>
);
}
}
There are many ways to handle this scenario. In this case, I recommend separating containers from components. The container will handle all things state and update its children components accordingly.
Please note that this example uses a lot of ES6 syntaxes. Please read the following to understand how some of it works: fat arrow functions, ES6 destruction, spread operator, ternary operator, class properties, a controlled react form utilizing event handlers and state, array filtering, and type checking with PropTypes.
It's a lot to take in, so if you have any questions, feel free to ask.
Working example:
containers/SeachForm
import React, { Component } from "react";
import moment from "moment";
import LearnMore from "../../components/LearnMore";
import Results from "../../components/Results";
import SearchBar from "../../components/Searchbar";
const data = [
{
id: "1",
name: "Bob",
age: 32,
email: "bob#example.com",
registered: moment("20111031", "YYYYMMDD").fromNow(),
description: "Bob is a stay at home dad."
},
{
id: "2",
name: "Jane",
age: 43,
email: "jane#example.com",
registered: moment("20010810", "YYYYMMDD").fromNow(),
description: "Jane is a CEO at Oracle."
},
{
id: "3",
name: "Yusef",
age: 21,
email: "yusef#example.com",
registered: moment("20180421", "YYYYMMDD").fromNow(),
description: "Yusef is a student at UC Berkeley."
},
{
id: "4",
name: "Dasha",
age: 29,
email: "dasha#example.com",
registered: moment("20050102", "YYYYMMDD").fromNow(),
description: "Dasha is an owner of a local antique shop."
},
{
id: "5",
name: "Polina",
age: 18,
email: "dasha#example.com",
registered: moment("20190102", "YYYYMMDD").fromNow(),
description: "Polina works at a local movie theather."
}
];
const initialState = {
searchQuery: "",
results: data, // <== change this to an empty array if you don't want to show initial user data
learnMore: false
};
class SearchForm extends Component {
state = { ...initialState }; // spreading out the initialState object defined above; it'll be the same as: "state = { searchQuery: "", results: data, learnMore: false }; "
handleSubmit = e => {
e.preventDefault(); // prevents a page refresh
if (!this.state.searchQuery) return null; // prevents empty search submissions
this.setState({
results: data.filter(
person => person.name.toLowerCase() === this.state.searchQuery.toLowerCase()
) // filters the dataset with the "searchQuery" (lowercased names) and returns the result if it finds a match
});
};
handleSearch = ({ target: { value } }) =>
this.setState({ searchQuery: value }); // updates searchQuery input with an event.target.value
handleReset = () => this.setState({ ...initialState }); // resets to initial state
handleLearnMore = person => {
this.setState({ learnMore: true, results: person }); // sets learnMore to true (to show the "LearnMore" component) and sets results to the selected user
};
render = () => (
<div className="container">
<SearchBar
handleReset={this.handleReset}
handleSearch={this.handleSearch}
handleSubmit={this.handleSubmit}
searchQuery={this.state.searchQuery}
/>
{!this.state.learnMore ? ( // if learnMore is false, then show "Results"
<Results
results={this.state.results}
handleLearnMore={this.handleLearnMore}
/>
) : (
<LearnMore {...this.state.results} /> // otherwise, show LearnMore
)}
</div>
);
}
export default SearchForm;
components/SearchBar
import React from "react";
import PropTypes from "prop-types";
const SearchBar = ({
handleReset,
handleSearch,
handleSubmit,
searchQuery
}) => (
<div className="search">
<div className="search-bar">
<form onSubmit={handleSubmit}>
<input
type="text"
className="uk-input"
value={searchQuery}
placeholder="Search for a name"
onChange={handleSearch}
/>
<div className="button-container">
<button
type="button"
className="uk-button uk-button-danger reset"
onClick={handleReset}
>
Reset
</button>
<button type="submit" className="uk-button uk-button-primary submit">
Submit
</button>
</div>
</form>
</div>
</div>
);
SearchBar.propTypes = {
handleReset: PropTypes.func.isRequired,
handleSearch: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
searchQuery: PropTypes.string
};
export default SearchBar;
components/Results
import React from "react";
import PropTypes from "prop-types";
const Results = ({ handleLearnMore, results }) => (
<div className="results">
{results && results.length > 0 ? (
results.map(person => (
<div key={person.id} className="uk-card uk-card-default uk-width-1-2#m">
<div className="uk-card-header">
<div className="uk-width-expand">
<h3 className="uk-card-title uk-margin-remove-bottom">
{person.name}
</h3>
</div>
</div>
<div className="uk-card-body">
<p>{person.description}</p>
</div>
<div className="uk-card-footer">
<button
onClick={() => handleLearnMore(person)}
className="uk-button uk-button-text"
>
Learn More
</button>
</div>
</div>
))
) : (
<div className="uk-placeholder">No users were found!</div>
)}
</div>
);
Results.propTypes = {
handleLearnMore: PropTypes.func.isRequired,
results: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
age: PropTypes.number,
email: PropTypes.string,
registered: PropTypes.string,
description: PropTypes.string
})
)
};
export default Results;
components/LearnMore
import React from "react";
import PropTypes from "prop-types";
const LearnMore = ({ name, email, age, description, registered }) => (
<div className="uk-card uk-card-default uk-card-body">
<h3 className="uk-card-header">{name}</h3>
<p>
<strong>Email</strong>: {email}
</p>
<p>
<strong>Registered</strong>: {registered}
</p>
<p>
<strong>Age</strong>: {age}
</p>
<p>
<strong>Job</strong>: {description}
</p>
</div>
);
LearnMore.propTypes = {
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
registered: PropTypes.string.isRequired,
description: PropTypes.string.isRequired
};
export default LearnMore;
You should do your onSubmit like this:
<form id= "learn-more-form" onSubmit={this.learnMore(obj)}>
<input type="submit" value="Learn More"/>
</form>
Then the function should be:
learnMore = (data) => (e) => {
e.preventDefault()
console.log(data) // probably setState with this data so you can display it when it, like this.setState({ currentMoreResults: data })
}

Unable to type into React input field

I am unable to type any input into my input field. I am using React, and have already set a handleChange and a handleSubmit function. The first two input fields, for 'name' and 'email', take input just fine. But for 'favoriteCity', it doesn't seem to work.
I am wondering if it is due to a MongoDB error that I am getting.
class UserPage extends Component {
state = {
user: [],
newUser: {
name: '',
email: '',
favoriteCity: ''
}
}
getAllUsers = () => {
axios.get('/api/users')
.then(res => {
this.setState({ user: res.data })
})
}
componentDidMount() {
this.getAllUsers()
}
handleChange = event => {
const newUser = { ...this.state.newUser };
newUser[event.target.name] = event.target.value;
this.setState({ newUser: newUser});
}
handleSubmit = event => {
event.preventDefault()
axios.post('/api/users', this.state.newUser)
.then(res => {
this.props.history.push(`/users/${res.data._id}`)
})
}
render() {
return (
<div>
{ /* This shows a list of All Users */ }
{this.state.user.map(user => (
<div key={user._id}>
<Link to={`/users/${user._id}`}>{user.name}</Link>
</div>
))}
<h1>New User Page</h1>
<form onSubmit={this.handleSubmit}>
<label>Name: </label>
<input
type="text"
name="name"
placeholder="Name?"
value={this.state.newUser.name}
onChange={this.handleChange}
/>
<label>Email: </label>
<input
type="text"
name="email"
placeholder="Email?"
value={this.state.newUser.email}
onChange={this.handleChange}
/>
<label>Favorite City: </label>
<input
type="text"
name="city"
placeholder="Favorite City?"
value={this.state.newUser.favoriteCity}
onChange={this.handleChange}
/>
<Button
type="submit"
value="Submit"
variant="contained"
color="primary"
>
Create User
</Button>
</form>
</div>
);
}
}
export default UserPage;
Please help.
Weird that email works fine, from what you posted your handleChange function is only updating the name on the newUser.
What you should see is what you type in all the inputs appear in the name input.
To fix this, you should probably have separate change handlers for each input:
handleNameChange
handleEmailChange
...
You should also consider storing name, email etc.. at the root of your state instead of nesting them in an object, that'll simplify the handler functions code.

Categories

Resources