I do not quite understand yet how react is working.
As far as I noticed I am supposed to handle detection of a change of form fields (for example angular and knockout detect fields changes with observables).
I have the following snippet in react:
constructor(props) {
super(props);
this.state = {
products: [],
branchId: document.getElementById('branchId').value
};
this.updateproduct = this.updateproduct.bind(this);
this.deleteproduct = this.deleteproduct.bind(this);
}
render() {
return (
<Container>
{
this.state.products.map(product => (
<div key={product.id}>
<div className="col-xs-12 col-sm-3">
<div className="user-label">Title</div>
<input type="text" name="title" className="form-control" defaultValue={product.title} onChange={this.handleChange} />
</div>
<div className="col-xs-12 col-sm-3">
<div className="user-label">Upload image</div>
<input type="text" name="image" className="form-control" defaultValue={product.image} onChange={this.handleChange} />
<img src={product.image} height="100" width="auto" style={{ marginTop: '5px' }} />
</div>
<div className="col-xs-12 col-sm-5">
<div className="user-label">Description</div>
<textarea name="description" className="form-control" rows="7" defaultValue={product.description} onChange={this.handleChange}>
</textarea>
</div>
<div className="col-xs-12 col-sm-1">
<div className="user-label"> </div>
<span className="btn btn-danger pull-right" data-btntype="delete" onClick={() => this.deleteproduct(product.id)}>
×
</span>
<span className="btn btn-success pull-right" data-btntype="update" onClick={() => this.updateproduct(this)} style={{ marginTop: '2px' }}>
✓
</span>
</div>
<div className="clearfix"></div>
</div>
))
}
</Container>
)
}
and don't quite know how to write a function that detect change of every field?
Also how can I handle the form update?
The above snippet is part of a bigger form (that's why form tag is not there) and I need to handle only the product object update above (not the entire form).
UPDATE
I applied the following solution based on #Kejt answer - with just a typo/ two corrections:
In the input:
onChange={e => this.handleChange(product.id, 'name', e.target.value) // Note 'this' must be added to the function whenever it's called
And then the handleChange method:
handleChange = (id, propertyName, value) => {
this.setState(
state => ({ // mapped 'products' need to be 'this.state.products'
products: this.state.products.map(product => product.id === id ? {...product, [propertyName]: value} : product)
})
)
}
Plus I had to add #babel/plugin-proposal-class-properties plugin to make the syntax working.
And in the class contructor added the usual declaration:
this.handleChange = this.handleChange.bind(this);
you already have onChange listener assigned to input onChange={this.handleChange}
you need to add handleChange method to your component
handleChange = event => {
console.log(event.target.value)
}
you can rebuild this method with adding product id and input name
onChange={e => handleChange(product.id, 'name', e.target.value)
and then in handleChange method
handleChange = (id, propertyName, value) => {
this.setState(
state => ({
products: products.map(product => product.id === id ? {...product, [propertyName]: value} : product)
})
}
Related
I have a book table page to list all the books in the database, mapped with their book ID. If I click the book name, it will link to an edit book page, in this page, the input fields should be filled with the book detail, I can edit the book detail and save it in the database.
The problem is, how do I pass the book detail to another component?
Book table component
class BookTable extends Component {
constructor(props){
super(props)
this.state = {
books: [],
book: {}
}
render() {
return (
<div className='display-container'>
<div className="card-group-admin">
<div className="container">
<div className="row row-cols-3">
{this.state.books.map(book =>
<div className="card" style={{height:550 + 'px', width:320 + 'px'}} key={book._id}>
<img src={`http://localhost:3001/${book.bookImage}`} className="card-img-top" alt="comic book coverpage" />
<div className="card-body">
<Link to={`/books/${book._id}`} book={this.setState={book}}><h5 className="card-title">{book.bookName}</h5></Link>
<p className="card-text">{book.bookDescription}</p>
<div className="card-bottom-container">
<p className="card-status">{book.bookStatus}</p>
<button type="button" onClick={() => this.handleDeleteClick(book._id)}
className="btn btn-secondary" id="btnLength">Delete</button>
</div>
</div>
</div>)
}</div>
</div>
</div>
</div>
I am not sure whether "book={this.setState={book}}" inside the tag is correct or not, that's probably is where the problem at.
Edit component
import React, {useState} from 'react';
import { useHistory } from "react-router-dom";
import {book} from './BookTable';
function Edit ({book}) {
const [bookName, setBookName] = useState(book.bookName);
const [author, setAuthor] = useState(book.author);
const [publisher, setPublisher] = useState(book.publisher);
const [yearReleased, setYearReleased] = useState(book.yearReleased);
const [type, setType] = useState(book.type);
const [advancedBookType, setAdvancedBookType] = useState(book.advancedBookType);
const [rentalPrice, setRentalPrice] = useState(book.rentalPrice);
const [bookDescription, setBookDescription] = useState(book.bookDescription);
const handleSubmit = (e) => {
e.preventDefault();
const bookDetail = {
bookName,
author,
publisher,
yearReleased,
type,
advancedBookType,
rentalPrice,
bookDescription };
axios.put(`http://localhost:3001/app/books/${_id}`, bookDetail)
.then(response => {
console.log(response.data);
})
.catch(err => console.log(err));
}
const history = useHistory();
const handleBack = () => {
history.push("/adminDashboard");
}
return (
<div className="editPage">
<h1>Edit book page</h1>
<form onSubmit={handleSubmit} >
<label className="editLabel">Book name:</label>
<input type = 'text'
value={book.bookName}
onChange={(e) => setBookName(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Author:</label>
<input type = 'text'
value={book.author}
onChange={(e) => setAuthor(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Publisher:</label>
<input type = 'text'
value={book.publisher}
onChange={(e) => setPublisher(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Year released:</label>
<input type = 'number'
value={book.yearReleased}
onChange={(e) => setYearReleased(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Type:</label>
<select type = 'text'
value={book.type}
onChange={(e) => setType(e.target.value)}
className="form-control form-group form-control-sm input" id="exampleFormControlSelect1">
<option>Comedy</option>
<option>Love</option>
<option>Horror</option>
<option>Detecting</option>
<option>Fiction</option>
<option>Adventure</option>
<option>Action</option>
<option>Youth</option>
</select>
<label className="editLabel">Advanced book type:</label>
<select type = 'text'
value={book.advancedBookType}
onChange={(e) => setAdvancedBookType(e.target.value)}
className="form-control form-group form-control-sm input" id="exampleFormControlSelect1">
<option >None</option>
<option>Popular</option>
<option>New release</option>
</select>
<label className="editLabel">Rental price ($):</label>
<input type = 'number'
value={book.rentalPrice}
onChange={(e) => setRentalPrice(e.target.value)}
className="form-control form-group form-control-sm input" />
<label className="editLabel">Book description:</label>
<textarea value={book.bookDescription}
onChange={(e) => setBookDescription(e.target.value)}
className="form-control form-group textbox" rows="4">
</textarea>
<div className="buttonGroup">
<button onClick={() => handleBack()} className="btn btn-outline-dark">Back</button>
<input type="submit" className="btn btn-primary" id="right" value="Save" />
{/* <input type="reset" className="btn btn-outline-secondary" id="right" value="Delete" /> */}
</div>
</form>
</div>
);
}
export default Edit;
Backend book route
router.put('/books/:id', (request, response) => {
Book.findOneAndUpdate({ _id: request.params.id }, request.body, { new:true, useFindAndModify: false },
(err, book) => {
if (err) {
response.send(err);
}
response.json(book);
});
})
<Link to={{ pathname: `/books/${book._id}`, state: { data: book } }}> {book.bookName} </Link>
in Edit component
const { data } = props.location.state;
I've been trying to create a form that can add a list of objects with multiple attributes to a list. I managed to get this right with one string attribute. But, I cannot figure out how to add an entire object with property values passed from the form. I'm using functional components to do this........How can I create a form that adds new objects of items to a list? I'm fairly new to ReactJS, btw.
resume.jsx
function App() {
const [jobExperience, setjobExperience] = useState([{
jobCompanyName: '',
jobDateRangeStart: '',
jobDateRangeEnd: '',
jobDescription: '',
reference_up_count: 0,
reference_down_count: 0,
}]);
const refUpvoteCount = index => {
const newReferences = [...jobExperience];
newReferences[index].reference_upvote_count++;
setjobExperience(newReferences)
}
const refDownvoteCount = index => {
const newReferences = [...jobExperience];
newReferences[index].reference_downvote_count++;
setjobExperience(newReferences)
}
return(
<Container className="container-fluid g-0">
<Row>
<Col>
<div>
{jobExperience.map((jobExp, index) => (
<JobExperience key={index} jobExperience={jobExp} refUpvote={refUpvoteCount} refDownvote={refDownvoteCount}
))}
</div>
</Col>
<Col>
<div className="pl-5 pr-5 pb-2">
<form onSubmit={//Add To Array of item Objects}>
<div className="form-group">
<label>Company Name</label>
<input type="text" className="form-control" placeholder="Add Company Name" name="jobCompanyName" onChange={handleJobExperienceChange} />
</div>
<div className="form-row">
<div className="col">
<div className="form-group">
<label>Start Date</label>
<Datetime dateFormat="YYYY" timeFormat={false} onChange={(date) => setstartDate(date.year())} value={jobExperience.jobDateRangeStart} />
</div>
</div>
<div className="col">
<div className="form-group">
<label>End Date</label>
<Datetime dateFormat="YYYY" name="jobDateRangeEnd" timeFormat={false} onChange={(date) => setendDate(date.year())} value={jobExperience.jobDateRangeEnd} />
</div>
</div>
</div>
<div className="pt-1">
<div className="form-group">
<label>Job Role/Responsibilities</label>
<textarea style={{width: '100%'}} name="jobDescription" onChange={handleJobExperienceChange} />
<button type="submit" onClick={handleJobExperienceAdd} className="btn btn-success btn-sm btn-block">Add Job Experience</button>
</div>
</div>
</div>
</form>
</Col>
</Row>
</Container>
)
}
function JobExperience({jobExperience, index, refUpvote, refDownvote}) {
return (
<div>
<Card style={{width: '18rem'}} className="remove-border-radius">
<Card.Body>
<Card.Title><span><i className="fa fa-building"></i> {jobExperience.jobCompanyName}</span></Card.Title>
</Card.Body>
<Card.Text>
<i className="fa fa-calendar"></i> {jobExperience.jobDateRangeStart}-{jobExperience.jobDateRangeEnd}
</Card.Text>
<Card.Text>
<span><i className="fa fa-info-circle"></i> {jobExperience.jobDescription}</span>
</Card.Text>
<Button variant="primary" onClick={() => refUpvote(index)} className="remove-border-radius"><i className="fa fa-plus"></i> Reference {jobExperience.reference_upvote_count}</Button>
<Button variant="danger" onClick={() => refDownvote(index)} className="remove-border-radius"><i className="fa fa-minus-circle"></i> Reference {jobExperience.reference_downvote_count}</Button>
</Card>
</div>
)
}
Change the way you set your state from this:
const refUpvoteCount = (index) => {
const newReferences = [...jobExperience];
newReferences[index].reference_upvote_count++;
setjobExperience(newReferences);
};
const refDownvoteCount = (index) => {
const newReferences = [...jobExperience];
newReferences[index].reference_downvote_count++;
setjobExperience(newReferences);
};
To this:
const refUpvoteCount = (index) => {
setjobExperience((previousState) => {
const newReferences = [...previousState];
newReferences[index].reference_upvote_count++;
return newReferences;
});
}
const refDownvoteCount = (index) => {
setjobExperience((previousState) => {
const newReferences = [...previousState];
newReferences[index].reference_downvote_count++;
return newReferences;
});
}
You may also take note the difference to understand this other way of setting-up state that needs to have the the value of the previous state
Do it like this.
const myFunction = () => {
setState((previousState)=> newState)
}
If you need to get the reference of the previous state pass a callback function on setState and that call back function can take 1 parameter which that represent the previous state. And on the callback function you can do some operations if you need to. The return value of callback function will be the new state
And not like this
const myFunction = () => {
const newState = state
setState(newState)
}
This last code sample reference the previous state the wrong way and will not work
const [form, setForm] = useState({}); // form is the previous jobExperience object
const onChange = (event) => {
const { name, value } = event.target;
let savedValue = value;
/*
condition your changes below, you can also extract
the content of the condition to separate functions
*/
if (name === 'jobDateRangeStart') {
savedValue = []; // whatever you need to do with the value
}
if (name === 'jobDateRangeEnd') {
savedValue = []; // whatever you need to do with the value
}
if (name === 'jobDateRangeEnd') {
savedValue = []; // whatever you need to do with the value
}
setForm({ ...form, [name]: savedValue });
};
return (
<div className="pl-5 pr-5 pb-2">
<div className="form-group">
<label>Company Name</label>
<input
className="form-control"
name="jobCompanyName"
onChange={handleChange}
placeholder="Add Company Name"
type="text"
value={form.jobCompanyName || ''}
/>
</div>
<div className="form-row">
<div className="col">
<div className="form-group">
<label>Start Date</label>
<Datetime
dateFormat="YYYY"
onChange={handleChange}
timeFormat={false}
value={form.jobDateRangeStart || ''}
/>
</div>
</div>
<div className="col">
<div className="form-group">
<label>End Date</label>
<Datetime
dateFormat="YYYY"
name="jobDateRangeEnd"
onChange={handleChange}
timeFormat={false}
value={form.jobDateRangeEnd || ''}
/>
</div>
</div>
</div>
<div className="pt-1">
<div className="form-group">
<label>Job Role/Responsibilities</label>
<textarea
name="jobDescription"
onChange={handleChange}
value={form.jobDescription || ''}
style={{width: '100%'}}
/>
<button
className="btn btn-success btn-sm btn-block"
onClick={handleChange}
type="submit"
>
Add Job Experience
</button>
</div>
</div>
</div>
);
As far as i understood you are trying to add an object into an array with multiple fields . and the value of object will come from the values of your form . Here's how can you do it.
# Step 1 :
first create a state that will hold the array of objects .
const [arrayOfObjects , setArrayOfObjects ] = useState([]) ; // empty array initially
# Step 2 :
grab the value from your form's submit function and create the object
onSubmitHandler = () => {
const newObject = {
property1 : "some value " // this values will come from your form
property2 : "some value " // depending on your implementation you may have to maintain separate state for each property
}
const newState = [ ...arrayOfObjects , newObject ];
setArrayOfObjects(newState);
}
I make simple task for getting job. Working with react and redux. When i get value from input and send them to reducer they are lost in the way. Wait, not so easy. 1st item getting by reducer gets prop name, age, type, index and return new state. Nice. But other items lost prop name and age in the way. What? How did them it? Reducer return empty obj for render. Dont look on obj in dispatch i will rework it.
REDUCER
case 'EDIT_ITEM':
console.log(action.name, action.age, action.id);
return state.map((item, index) =>
action.id === index
? {
name: action.name,
age: action.age
}
: item
);
App.js
function EditUsers() {
const listItems = users.map(function (value, index) {
return (
<form>
<div className="input-group">
<div className="input-group-prepend">
<span className="input-group-text">{value.name}, {value.age}</span>
</div>
<input type="text" placeholder="New name" id="newName" className="form-control"/>
<input type="text" placeholder="New age" id="newAge" className="form-control" aria-describedby="button-addon2"/>
<div className="input-group-append">
<button onClick={() => dispatch({
type: 'EDIT_ITEM',
id: index,
name: document.getElementById('newName').value,
age: document.getElementById("newAge").value
})}
className="btn btn-outline-primary"
type="button"
id="button-addon2">
Изменить
</button>
</div>
</div>
</form>
)
});
return (
<div>{listItems}</div>
)
}
You won't be able to access the input values from the button's onClick event, but if you decide to leave the inputs uncontrolled and move the logic to the associated form's onSubmit callback, then you can access the form's field values from the onSubmit event.
Define a submitHandler function to consume both the index and submit event, e:
const submitHandler = index => e => {
e.preventDefault(); // <-- prevent the default form action, important!
const { newName, newAge } = e.target; // <-- destructure the inputs from the event target
dispatch({
type: "EDIT_ITEM",
id: index,
name: newName.value, // <-- extract the input value
age: newAge.value // <-- extract the input value
});
};
Here the path to the input value is e.target.<fieldId>.value. Notice I've also defined submitHandler to curry the index, which allows for more optimal usage when mapping elements.
Next, attach the submitHandler callback to the onSubmit prop of the form.
const listItems = users.map(function(value, index) {
return (
<form key={index} onSubmit={submitHandler(index)}>
...
Here the curried function submitHandler(index) takes the index and encloses it in an instance of the callback, returning a function that takes the onSubmit event object, e => {....
Finally, update the button to have type="submit" and no onClick handler.
<button
className="btn btn-outline-primary"
type="submit"
id="button-addon2"
>
Изменить
</button>
Full code
const submitHandler = index => e => {
e.preventDefault();
const { newName, newAge } = e.target;
dispatch({
type: "EDIT_ITEM",
id: index,
name: newName.value,
age: newAge.value
});
};
function EditUsers() {
const listItems = users.map(function(value, index) {
return (
<form key={index} onSubmit={submitHandler(index)}>
<div className="input-group">
<div className="input-group-prepend">
<span className="input-group-text">
{value.name}, {value.age}
</span>
</div>
<input
type="text"
placeholder="New name"
id="newName"
className="form-control"
/>
<input
type="text"
placeholder="New age"
id="newAge"
className="form-control"
aria-describedby="button-addon2"
/>
<div className="input-group-append">
<button
className="btn btn-outline-primary"
type="submit"
id="button-addon2"
>
Изменить
</button>
</div>
</div>
</form>
);
});
return <div>{listItems}</div>;
}
Here's the code
render () {
if (this.props && this.props.list) {
return (
<div className="List_select">
{!this.state.editing
&& <div><button className="List_delete" onClick={(ev) => {
ev.preventDefault();
this.props.handleDelete(this.props.list);
}} />
<NavLink to={{
pathname: '/list/' + this.props.userId + '|' + this.props.list
}}>
{this.props.list}
</NavLink>
<button className="List_edit" onClick={(ev) => {
ev.preventDefault();
this.setState({editing: true})
}} /></div>
}
{this.state.editing
&& <form onSubmit={(ev) => {
ev.preventDefault();
this.props.handleEdit(this.props.list, this.state.listName);
this.setState({editing: false})
}}><input type="text" name="listName" className="List_input" value={this.props.list} onChange={(ev) => {
this.setState({listName: ev.target.value});
}} />
<button type="submit" value="submit" name="submit" className="StandardButton-1">Submit</button>
</form>
}
</div>
)
} else {
return ""
}
}
The code is not letting the user fill out the text field. Does anyone know what I'm doing wrong? I can edit the text fields at all. I have the onChange trigger and doing all the good stuff to handle text fields correct in the code.
You are giving it value but in onChange your changing another value.
Have the same value in both :)
<input type="text" name="listName" className="List_input"
value={this.state.listName}
onChange={(ev) => {this.setState({listName: ev.target.value}) }}
/>
value={this.props.list} onChange={(ev) => {
this.setState({listName: ev.target.value});
the listName and list must match, and you have to use this.state.list
You have to assign the text box value from state not props, because props is readonly. So you have to replace this.props.list with this.state.listName
<input
type="text"
name="listName"
className="List_input"
value={this.state.listName}
onChange={ev => {
this.setState({ listName: ev.target.value });
}}
/>
I have the following React component (using hooks), which lists a number of Tasks as a dropdown list. When an item is selected from the list, I want to display an Update form. This works only when an item is selected for the first time. When I select a new item, nothing happens (although console.log(e.target.value); prints the correct value). I store the selected task's id in st_taskId.
I wonder if you see any issues in the code below:
const ManageReviewTasks = props => {
const reviewRoundId = props.match.params.reviewRoundId;
const [st_taskId, set_taskId] = useState();
useEffect(() => {
if (props.loading == false && st_taskId == null)
props.fetchReviewTasksByReviewRound(reviewRoundId);
}, [reviewRoundId, st_taskId]);
if (props.loading == true) {
return <div>Loading...</div>;
}
return (
<>
{props.reviewTasks && (
<div>
<h3>Configure the Review Tasks</h3>
<br />
{
<div>
<div>
<h4>
Tasks for <span className="font-italic">students receiving</span> feedback:
</h4>
<select
className="form-control"
onChange={e => {
e.preventDefault();
console.log(e.target.value);
set_taskId(e.target.value);
}}>
<option>--SELECT--</option>
{Object.keys(props.reviewTasks).map(id => {
const task = props.reviewTasks[id];
{
if (task.isForStudent) {
return (
<option key={id} id={id} value={id}>
{task.title}
</option>
);
}
}
})}
</select>
</div>
{props.reviewTasks[st_taskId] && (
<UpdateReviewTaskForm task={props.reviewTasks[st_taskId]} />
)}
</div>
}
</div>
)}
</>
);
};
Below is the code for the UpdateReviewTaskForm component:
const UpdateReviewTaskForm = (props) => {
const [st_Title, set_Title] = useState(props.task.title);
const [st_Description, set_Description] = useState(RichTextEditor.createValueFromString(props.task.description, 'html'));
const [st_startDate, set_startDate] = useState(new Date(props.task.startDate.replace('-', '/')));
const [st_DueDate, set_DueDate] = useState(new Date(props.task.dueDate.replace('-', '/')));
const handleCancelClick = (event) => {
event.preventDefault();
history.goBack();
}
const onSubmit_saveTask = (e) => {
e.preventDefault();
props.updateReviewTask({
Id: props.task.id,
Title: st_Title,
Description: st_Description.toString('html'),
StartDate: format(st_startDate, 'DD/MM/YYYY'),
DueDate: format(st_DueDate, 'DD/MM/YYYY'),
})
}
if (props.loading)
return <div>Updating...</div>
return (
<div>
<br/>
<br/>
<div className="p-3 bg-light">
<h3 className="text-info">Update the Task:</h3>
{
props.task &&
<form onSubmit={onSubmit_saveTask}>
<div className="form-group">
<label>Enter the title</label>
<input
//placeholder="Enter a title..."
value={st_Title}
onChange={(event) => { set_Title(event.target.value) }}
className="form-control" />
</div>
<div className="form-group">
<label>Enter a description for the assessment</label>
<RichTextEditor
value={st_Description}
onChange={set_Description}
/>
</div>
<div className="form-group">
<label>Start date to start: </label>
<DatePicker
className="form-control"
selected={st_startDate}
onChange={(date) => set_startDate(date)}
/>
</div>
<div className="form-group">
<label>Due date to complete: </label>
<DatePicker
className="form-control"
selected={st_DueDate}
onChange={(date) => set_DueDate(date)}
/>
</div>
<br />
<button type="submit" className="btn btn-primary">Submit</button>
<button type="reset" className="btn btn-light" onClick={handleCancelClick}>Cancel</button>
</form>
}
</div>
</div>
)
}
Because you are using internal state in UpdateReviewTaskForm, even if this component re-render for the second time, its state will not be reset (to the default value props.task.title for example).
One way to force the state to reset is to use a key prop in UpdateReviewTaskForm like this :
{props.reviewTasks[st_taskId] && (
<UpdateReviewTaskForm key={st_taskId} task={props.reviewTasks[st_taskId]} />
)}
Another way is to use useEffect inside UpdateReviewTaskForm to run when props.task change
useEffect(() => {
// reset the state here
}, [props.task])