I'm trying to dynamically add inputs when the user clicks the button to add a question.
Usually doing a controlled form is easy as your know what the field names are. But in this situation they are dynamic.
I've got a working solution but it mutates the state.
Is there a better way to do this?
Thanks
JSX
import React, { Component } from 'react';
import axios from 'axios';
import { saveAs } from 'file-saver';
class Form extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.handleForm = this.handleForm.bind(this);
this.addQuestion = this.addQuestion.bind(this);
this.removeQuestion = this.removeQuestion.bind(this);
this.state = {
questions: []
}
}
onChange(e, i) {
this.state.questions[i] = e.target.value;
this.setState({
questions: this.state.questions
})
}
handleForm(e) {
e.preventDefault();
const body = {
questions: this.state.questions
};
axios.post('/api/pdfs/create', body)
.then(() => axios.get('/api/pdfs/fetch', { responseType: 'blob' }))
.then((res) => {
const pdfBlob = new Blob([res.data], { type: 'application/pdf' });
return saveAs(pdfBlob, 'questions.pdf');
})
.catch(error => {
console.log(error.response)
});
}
addQuestion() {
this.setState({
questions: [...this.state.questions, '']
});
}
removeQuestion(index) {
this.setState({
questions: this.state.questions.filter((question, i) => i !== index)
});
}
render() {
return (
<div>
<button onClick={this.addQuestion}>Add Question</button>
<form onSubmit={this.handleForm}>
{this.state.questions.map((question, index) => (
<div key={index}>
<input type="text" name={`question-${question}`} onChange={(e) => this.onChange(e, index)} />
<button type="button" onClick={() => this.removeQuestion(index)}>x</button>
</div>
))}
<button type="submit">Submit</button>
</form>
</div>
);
}
}
export default Form;
You are mutating the state only in your onChange call, and that can be fixed easily:
onChange(e, i) {
this.setState({
questions: this.state.questions.map((v, i2) => i === i2 ? e.target.value : v),
});
}
(This won't change the functionality though, its just a "best practice improvement")
Related
I'd like to store todo data with localStorage so that it won't disappear after refreshing the page.
I used React class component when started creating.
I've added 'handleFormSubmit' and 'ComponentDidMount' methods.
nothing stores in localStorage when I type todo and choose date.
get an error in ComponentDidMount with
Line 'const result = localData ? JSON.parse(localData) : [];'
:SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
how can I set and get items?
It would be really appreciated if I could get help.
I'd like to make this app really work.
import React from "react"
import TodoItem from "./components/TodoItem"
import todosData from "./components/todosData"
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
todos: todosData,
//setTodos: todosData,
newItem: "",
deadline: "",
editing: false
}
this.handleChange = this.handleChange.bind(this)
this.addTodo = this.addTodo.bind(this)
this.updateInput = this.updateInput.bind(this)
this.deleteItem = this.deleteItem.bind(this)
this.updateItem = this.updateItem.bind(this)
this.updateDeadline = this.updateDeadline.bind(this)
this.updateInputDeadline = this.updateInputDeadline.bind(this)
this.editItem = this.editItem.bind(this)
this.handleFormSubmit = this.handleFormSubmit.bind(this)
}
handleChange(id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
} else {
return todo;
}
});
return { todos: updatedTodos };
});
}
addTodo(e) {
e.preventDefault();
const newTodo = {
id: this.state.todos.length + 1,
text: this.state.newItem,
completed: false,
deadline: this.state.deadline
}
const newTodos = this.state.todos.concat([newTodo]);
this.setState({
todos: newTodos
})
}
updateInput(value, id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
if(todo.id === id) {
return {...todo, text: value}
}else {
return todo;
}
})
return {todos: updatedTodos}
})
}
updateInputDeadline(value, id) {
this.setState((prevState) => {
const updatedTodos = prevState.todos.map((todo) => {
if(todo.id === id) {
console.log(value, id);
return {...todo, deadline: value}
}else {
return todo;
}
})
return {todos: updatedTodos}
})
}
updateItem(e) {
this.setState({
newItem: e.target.value
})
}
updateDeadline(e) {
this.setState({
deadline: e.target.value
})
}
deleteItem(id){
const filteredItems= this.state.todos.filter(item =>
item.id!==id);
this.setState({
todos: filteredItems
})
}
editItem(id) {
this.setState({
editing: id
})
}
handleFormSubmit() {
const { todo, deadline } = this.state;
localStorage.setItem('todo', JSON.stringify(todo));
localStorage.setItem('deadline', deadline);
};
componentDidMount() {
const localData = localStorage.getItem('todo');
const result = localData ? JSON.parse(localData) : [];
const deadlineData = localStorage.getItem('deadline');
this.setState({ result, deadlineData });
}
render() {
const todoItems = this.state.todos.map
(item =>
<TodoItem
key={item.id}
item={item}
handleChange={this.handleChange}
addTodo={this.addTodo}
deleteItem={this.deleteItem}
updateInput={this.updateInput}
updateInputDeadline={this.updateInputDeadline}
isEdited={this.state.editing === item.id}
editItem={this.editItem}
/>)
return (
<div className="todo-list">
<Timer />
<form onSubmit={this.handleFormSubmit}>
<div className="add-todo">
<label>Add an item...</label>
<input
type="text"
name="todo"
placeholder="Type item here..."
value={this.state.newItem}
onChange={this.updateItem}
/>
</div>
<div className="date">
<label htmlFor="deadline">Deadline</label>
<input
type="date" id="start" name="deadline"
min="2021-01-01"
max="2024-12-31"
value={this.state.deadline}
onChange={this.updateDeadline}
/>
</div>
<button type="submit" onClick={this.addTodo}>Add to the list</button>
</form>
{todoItems.length === 0 ? <p>No items</p> : null}
<div className="todoitems">
{todoItems}
</div>
</div>
)
}
}
export default App
When you press the button, there are two events that you are trying to call - addTodo and handleFormSubmit. Since you are calling e.preventDefault() in addTodo, the submit event is never called. You could do all of the actions you need in one of the methods.
My guess is that you are either trying to JSON.parse an array instead of an object, or the value of todo is undefined. You are trying to get todo out of this.state, but you only have todos in your state, so it might be a typo. The same goes for deadline.
You are doing the setting and getting correctly. You could actually get data from localStorage even when you are first setting the state in constructor. But the componendDidMount approach you tried is also good.
constructor(props) {
super(props)
const cachedTodos = localStorage.getItem("todo")
this.state = {
todos: cachedTodos ?? todosData,
...
}
I have two pages on my react app. One page allows you to submit a post, and the second page shows all of the posts. I need to be able to retrieve the data from the state on one page, but I am receiving an error. What am I doing wrong to display this, because I thought I could use props to gather the state from my post page.
My Display Post Page:
import React from 'react';
import './App.css';
export default class Scroll extends React.Component {
render() {
return (
<div className="flex-container">
<div className="post">
{this.props.displayPost(this.props.state.posts)}
</div>
</div>
);
}
}
My post page:
import React from 'react';
import axios from 'axios';
import './App.css';
import { post } from '../../routes/routes';
export default class PersonList extends React.Component {
state = {
title: "",
body: "",
posts: []
};
componentDidMount = () => {
this.getPost();
}
getPost = () => {
axios.get("http://localhost:5000/posts/save")
.then((response) => {
const data = response.data;
this.setState({ posts: data });
console.log("Data has been recieved")
})
.catch(() => {
alert("Error recieving data")
})
}
handleChange = (event) => {
const target = event.target;
const name = target.name;
const value = target.value;
this.setState({
[name]: value
})
};
submit = (event) => {
event.preventDefault();
const payload = {
title: this.state.title,
body: this.state.body,
}
axios({
url: 'http://localhost:5000/posts/save',
method: 'POST',
data: payload,
})
.then(() => {
console.log('Data sent to the server');
})
.catch(() => {
console.log('Internal server error');
});
};
displayPost = (posts) => {
if (!post.length) return null;
return posts.map((post, index) => {
<div key={index}>
<h3 id="post-text">{post.title}</h3>
<p id="post-text">{post.body}</p>
</div>
});
}
render() {
console.log("State ", this.state)
return (
<div className="flex-container-home">
<div className="app">
<form onSubmit={this.submit}>
<input
placeholder="title"
type="text"
name="title"
value={this.state.title}
onChange={this.handleChange}
/>
<textarea placeholder="description"
name="body"
cols="30" rows="10"
value={this.state.body}
onChange={this.handleChange}
>
</textarea>
<button>Submit</button>
</form>
</div>
</div>
)
}
}
Here is working example:
import React from "react";
export default class PersonList extends React.Component {
state = {
title: "",
body: "",
posts: [],
};
componentDidMount = () => {
this.getPost();
};
getPost = () => {
this.setState({ posts: ["post1", "post2", "post3"] });
};
displayPost = (posts) => {
if (!posts || !posts.length) return null;
return posts.map((post, index) => (
<div key={index}>
<p>{post}</p>
</div>
));
};
render() {
return (
<div className="App">
<Scroll displayPost={this.displayPost} posts={this.state.posts} />
</div>
);
}
}
class Scroll extends React.Component {
render() {
return (
<div className="post">
Posts: {this.props.displayPost(this.props.posts)}
</div>
);
}
}
I asked similar question earlier, but didn't get much back. I have two modals for user auth: join and login. Each modal has a link to the other one. Displayed login errors persist when you click on the "sign up" and switch to the join modal and vise versa. I tried to set the state.errors to empty array, but the errors still persist. I changed handleSwitch to callback. The errors array still has length. I tried using switched as part of the state, resetting it to true in handleSwitch and ternary, no result either. Can anybody suggest an alternative solution.
import React from 'react';
class Login extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
errors: [],
switched: false
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleSwitch = this.handleSwitch.bind(this);
this.mapErrors = this.mapErrors.bind(this);
this.handleErrors = this.handleErrors.bind(this);
}
componentDidMount() {
this.setState({ errors: this.props.errors})
}
componentDidUpdate(prev) {
if (prev.errors.length !== this.props.errors.length) {
this.setState( {errors: this.props.errors} )
}
}
handleInput(type) {
return (err) => {
this.setState({ [type]: err.currentTarget.value })
};
}
handleSubmit(event) {
event.preventDefault();
const user = Object.assign({}, this.state);
this.props.processForm(user)
// .then(() => this.props.history.push('/users')); //change to /videos later
}
handleSwitch() {
// debugger
this.setState({ errors: [] }, function () {
this.props.openModal('signup')
});
// debugger
}
mapErrors() {
if (this.state.errors.length) {
return this.state.errors.map((error, i) => {
return <p key={i}>{error}</p>
})
}
}
handleErrors() {
debugger
if (!this.state.switched) {
return <div className="errors">{this.mapErrors}</div>
} else {
return null;
}
};
render() {
console.log(this.state.errors)
return (
<div className="login-form">
<div>
<h2 className="login-header">Log in to Foxeo</h2>
</div>
<form>
<input className="login-email"
type="text"
value={this.state.email}
placeholder="Email address"
onChange={this.handleInput('email')}
/>
<input className="login-password"
type="password"
value={this.state.password}
placeholder="Password"
onChange={this.handleInput('password')}
/>
<div className="errors">{this.mapErrors()}</div>
{/* { this.state.switched ?
<div className="errors">{this.handleErrors()}</div> :
<div className="errors">{this.mapErrors()}</div>
} */}
<button className="login-button" onClick={this.handleSubmit}>Log in with email</button>
<div className="login-footer">Don't have an account?
{/* <button className="login-form-btn" onClick={() => this.props.openModal('signup')}>Join</button> */}
<button className="login-form-btn" onClick={ this.handleSwitch}> Join</button>
</div>
</form>
</div>
);
}
};
export default Login;
I suggest getting the new errors from the props instead of from state:
mapErrors() {
if (this.props.errors.length) {
return this.props.errors.map((error, i) => {
return <p key={i}>{error}</p>
})
Dispatching resetErrors action solved the issue. The handleSwitch method is quite simple:
handleSwitch() {
this.props.resetErrors()
this.props.openModal('signup')
}
session actions:
import * as apiUtil from '../util/session_api_util';
export const RECEIVE_CURRENT_USER = 'RECEIVE_CURRENT_USER';
export const LOGOUT_CURRENT_USER = 'LOGOUT_CURRENT_USER';
export const RECEIVE_ERRORS = 'RECEIVE_ERRORS';
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
const receiveErrors = (errors) => ({
type: RECEIVE_ERRORS,
errors
})
const clearErrors = () => ({
type: CLEAR_ERRORS,
errors: []
})
const receiveCurrentUser = (user) => ({
type: RECEIVE_CURRENT_USER,
user
});
const logoutCurrentUser = () => ({
type: LOGOUT_CURRENT_USER
});
export const signup = user => dispatch => (
apiUtil.signup(user).then(user => (
dispatch(receiveCurrentUser(user))
), err => (
dispatch(receiveErrors(err.responseJSON))
))
);
export const login = user => dispatch => {
return apiUtil.login(user).then(user => {
dispatch(receiveCurrentUser(user))
}, err => (
dispatch(receiveErrors(err.responseJSON))
))
};
export const logout = () => dispatch => apiUtil.logout()
.then(() => dispatch(logoutCurrentUser()));
export const resetErrors = () => dispatch(clearErrors());
session errors reducer:
import { RECEIVE_ERRORS, RECEIVE_CURRENT_USER, CLEAR_ERRORS } from '../actions/session_actions';
const sessionErrorsReducer = (state = [], action) => {
Object.freeze(state);
switch (action.type) {
case RECEIVE_ERRORS:
return action.errors;
case CLEAR_ERRORS:
return [];
case RECEIVE_CURRENT_USER:
return [];
default:
return state;
}
};
export default sessionErrorsReducer;
How do I remove an item from an array in react? I've tried a couple of things and it didnt work out. Just trying to make a basic todo app. I've updated my post to provide the render method to show where the deleteTodo is going. I've also updated my deleteTodo with an answer I got from this post. It kind of works, the only problem is it deletes all of the items in the todo list rather than just the single one.
class App extends Component {
state = {
inputValue: "",
todos: [{
value: "walk the dog",
done: false
},
{
value: "do the dishes",
done: false
}
]
}
addTodo = (e) => {
this.setState({
inputValue: e.target.value
});
}
handleSubmit = (e) => {
e.preventDefault();
// console.log('clicked')
const newTodo = {
value: this.state.inputValue,
done: false
}
const todos = this.state.todos;
todos.push(newTodo);
this.setState({
todos,
inputValue: ''
})
}
deleteTodo = (value) => {
// Take copy of current todos
const todos = [this.state.todos];
const filteredTodos = todos.filter((item) => item.value !== value);
this.setState({
todos: filteredTodos
})
}
render() {
return (
<div className="App">
<Form
addTodo={this.addTodo}
handleSubmit={this.handleSubmit}
/>
<List
todos={this.state.todos}
deleteTodo={this.deleteTodo}
/>
</div>
);
}
}
export default App;
You just need to filter that value from array and set new filtered array in the setState.
deleteTodo = (value) => {
// Take copy of current todos
const todos = [...this.state.todos];
const filteredTodos = todos.filter( (item) => item.value !== value);
this.setState({
todos: filteredTodos
})
}
Your use of filter appears to be the problem.. To create a new array of elements without the value using filter, you can try something like this:
EDIT: Updated answer with a full working demo
import React, {Component} from 'react';
import './App.css';
class Form extends Component {
constructor(props) {
super(props);
this.textRef = React.createRef();
}
render() {
return (
<form onSubmit={(e)=>{e.preventDefault(); this.props.handleSubmit(this.textRef.current.value)}}>
<input type="text" ref={this.textRef}/>
<input type="submit" value="add"/>
</form>
);
}
}
class List extends Component {
render() {
return (
<ul>
{
this.props.todos.map((todo) => (
<li key={todo.value}>
<p><input type="checkbox" checked={todo.done}/>{todo.value} </p>
<input type="button" onClick={() => (this.props.deleteTodo(todo.value))} value="delete"/>
</li>
))
}
</ul>
);
}
}
class App extends Component {
state = {
inputValue: "",
todos: [{
value: "walk the dog",
done: false
},
{
value: "do the dishes",
done: false
}
]
}
addTodo = (e) => {
this.setState({
inputValue: e.target.value
});
}
handleSubmit = (value) => {
const newTodo = {
value,
done: false
}
const todos = [...this.state.todos];
todos.push(newTodo);
this.setState({
todos,
inputValue: ''
})
}
deleteTodo = (value) => {
const todos = this.state.todos;
const filteredTodos = todos.filter((item) => item.value !== value);
this.setState({
todos: filteredTodos
})
}
render() {
return (
<div className="App">
<Form
addTodo={this.addTodo}
handleSubmit={this.handleSubmit}
/>
<List
todos={this.state.todos}
deleteTodo={this.deleteTodo}
/>
</div>
);
}
}
export default App;
Also, I changed your handleSubmit method to create a new Array to keep in line with React's functional paradigm
i have used lodash for such this.
lodash is a library for doing such thing https://lodash.com/
if you can get the same value object that you inserted while adding the rest is quite easy
you lodash to find the index in which you have the object in the array
on your delete function
const todos = this.state.todos;
const itemToRemove = {value: "walk the dog",done: false};
var index = _.findIndex(todos, itemToRemove);
const filteredTodos = todos.splice(index, 1)
this.setState({
todos: filteredTodos
})
Hope this will help
I have two dropdowns, Discount Type and Offer Type, Discount type returns four elements so what I want to do is for example if I select option number 2 from that dropdown then call the URL that populates the Offer Type dropdown with the selected index, in this case '2', because now the offer type is returning all because I'm using the following URL that brings all: http://xxxxxx:8080/services/OfferType/getAll but instead of getAll I want to pass the index of the Offer Type Dropdown to have something like this http://xxxxxx:8080/services/OfferType/2
Any help on how to do this because I don't, below you'll find my current code:
import React from 'react';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
import Divider from 'material-ui/Divider';
import cr from '../styles/general.css';
export default class ExampleDropdown extends React.Component {
constructor(props) {
super(props);
this.state = {
DiscountTypeData: [],
OfferTypeData: [],
DiscountTypeState: '',
OfferTypeState: ''
};
this.handleChange = this.handleChange.bind(this);
this.renderDiscountTypeOptions = this.renderDiscountTypeOptions.bind(this);
this.renderOfferTypeOptions = this.renderOfferTypeOptions.bind(this);
this.handleChangeDiscountType = this.handleChangeDiscountType.bind(this);
this.handleChangeOfferType = this.handleChangeOfferType.bind(this);
}
componentDidMount() {
const offerTypeWS = 'http://xxxxxx:8080/services/OfferType/getAll';
const discountTypeWS = 'http://xxxxxx:8080/services/DiscountType/getAll';
fetch(offerTypeWS)
.then(Response => Response.json())
.then(findResponse => {
console.log(findResponse);
this.setState({
OfferTypeData: findResponse.offerTypes
});
});
fetch(discountTypeWS)
.then(Response => Response.json())
.then(findResponse => {
console.log(findResponse);
this.setState({
DiscountTypeData: findResponse.discountTypes
});
});
}
handleChange(event, index, value) {
this.setState({value});
}
handleChangeDiscountType(event, index, value) {
this.setState({ DiscountTypeState: (value) });
}
handleChangeOfferType(event, index, value) {
this.setState({ OfferTypeState: (value) });
}
renderDiscountTypeOptions() {
return this.state.DiscountTypeData.map((dt, i) => {
return (
<MenuItem
key={i}
value={dt.text}
primaryText={dt.text} />
);
});
}
renderOfferTypeOptions() {
return this.state.OfferTypeData.map((dt, i) => {
return (
<MenuItem
key={i}
value={dt.offerTypeDesc}
primaryText={dt.offerTypeDesc} />
);
});
}
render() {
return (
<div className={cr.container}>
<div className ={cr.boton}>
<Divider/>
<br/>
</div>
<div>
<DropDownMenu
value={this.state.DiscountTypeState}
onChange={this.handleChangeDiscountType}>
<MenuItem value={''} primaryText={'Select discount type'} />
{this.renderDiscountTypeOptions()}
</DropDownMenu>
<br/>
<DropDownMenu
value={this.state.OfferTypeState}
onChange={this.handleChangeOfferType}>
<MenuItem value={''} primaryText={'Select offer type'} />
{this.renderOfferTypeOptions()}
</DropDownMenu>
</div>
</div>
);
}
}
This is the response from the Discount Type service:
So if I select "Bulk Discount" that has the value "3" then I want to pass that 3 to the Offer Type URL..
You can call fetch from handleChangeDiscountType or handleChangeOfferType just like you called in componentDidMount. Example:
handleChangeDiscountType(event, index, value) {
fetch('http://xxxxxx:8080/services/DiscountType/' + value.id)
.then(Response => Response.json())
.then(findResponse => {
console.log(findResponse);
this.setState({ DiscountTypeState: findResponse });
});
}