Adding new inputs to component and tracking state in React - javascript

I have a React component that has a few simple input fields where I am currently tracking the state to eventually be placed into an AJAX call. I also have a button that, on click, will create a new row of input fields (same as the initial inputs).
I am pretty new to React and initially built out a simple function that clones the entire div and appends it to the .ticket-section div. I ran into some problems about inputs having the same react-id and it honestly felt like I was fighting the framework a little.
Any recommendations of how to create these new inputs and be able to track the state individually of the new row of inputs? Greatly appreciated in advance.
Here is my component:
var AddItem = React.createClass({
getInitialState: function() {
return {item_name: '', quantity: '', price: ''}
},
itemNameChange: function(e) {
this.setState({item_name: e.target.value});
},
quantityChange: function(e) {
this.setState({quantity: e.target.value});
},
priceChange: function(e) {
this.setState({price: e.target.value});
},
render: function() {
return (
<div>
<div className="ticket-section">
<div className="add-ticket">
<ul>
<li>
<label>Name</label>
<input id="item-name" type="text" placeholder="xyz item" value={this.state.item_name} onChange={this.itemNameChange} />
</li>
<li>
<label>Quantity Available</label>
<input id="quantity" type="number" placeholder="100" value={this.state.quantity} onChange={this.quantityChange} />
</li>
<li>
<label>Price</label>
<input id="price" type="number" placeholder="25.00" value={this.state.price} onChange={this.priceChange} />
</li>
</ul>
</div>
</div>
<button className="add-another-item">+ Add another item</button>
</div>
);
}
});
Thanks again.

I'm not sure but let me quess, are you looking for something like this?
class InputComponent extends React.Component {
constructor(props){
super(props)
}
render(){
return <div>
<input type="text"
onChange={this.props.change}/>
</div>
}
}
class Widget extends React.Component {
constructor(){
this.state = {
values: ['']
};
this.handleClick = this.handleClick.bind(this);
}
handleChange(index, e){
const oldState = this.state.values;
oldState[index] = e.target.value
this.setState({values: oldState})
}
handleClick(){
const oldState = this.state.values
oldState.push('');
this.setState({values: oldState})
}
render(){
const itemList = this.state.values.map((item, index)=>{
return <InputComponent key={index} change={this.handleChange.bind(this, index)}/>
});
console.log(this.state.values)
return <div>
{itemList}
<hr/>
<button onClick={this.handleClick}>Click</button>
</div>
}
}
React.render(<Widget />, document.getElementById('container'));
Fiddle example. I hope it will help you.
Thanks

Related

Implement recursive onClick event in react js

I am trying to implement a recursive method on reactjs, but only when data is submitted or clicked.
Following is the code I am trying with:
import React, { Component } from "react";
class Textbox extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {this.setState({value: event.target.value}); }
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="React" />
</div>
</form>
);
}
}
export default Textbox;
which generates the following view.
I want to use a recursive method onClick or onSubmit, such that it will generate another text box upon submission. Like
which I want to continue until I click some "Exit" or "Stop" button, which I can add to the top of the view.
From what I have read about recursive implementation on ReactJS, I need to call the class/function again inside render. When I do that I think react is getting inside the infinite loop and freezes the browser.
what I tried is to call Textbox inside the <div> <div/> of the render method. Like this:
.... other code lines are same
<div>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="React" />
<Textbox/>
</div>
How can I generate a recursive textbox on submission/clicking event on the previous text box?
You could do it like this, where showNextInput prevents the infinite loop:
class Textbox extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
showNextInput: false,
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {this.setState({value: event.target.value}); }
handleSubmit(event) {
console.log('submit');
event.preventDefault();
this.setState({ showNextInput: true });
}
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
<div>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</div>
</form>
{ this.state.showNextInput ? <Textbox/> : null }
</>
);
}
}
However, your use case looks like something you would usually do by
managing a list of values somewhere,
add items as required inside your handlers, and
then display a list of these items
Here a quick and dirty example:
export class TextboxList extends React.Component {
constructor(props) {
super(props);
this.state = {
values: {
0: ''
},
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(index, value) {
this.setState({
values: {
...this.state.values,
[index]: value
}
});
}
handleSubmit(event) {
event.preventDefault();
this.setState({
values: {
...this.state.values,
[Object.keys(this.state.values).length]: '' // add new empty item to list
}
});
}
render() {
return (
<>
<form onSubmit={this.handleSubmit}>
{ Object.keys(this.state.values).map( (index) => {
const value = this.state.values[index];
return <div key={ index }>
<label>
Name:
<input
type="text"
value={ value }
onChange={ (event) => this.handleChange( index, event.target.value ) }
/>
</label>
<input type="submit" value="Submit" />
</div>;
})}
</form>
</>
);
}
}
export default Textbox;

Issue with unique key props for child elements

I have a generic Todo List built in React. Each task from user input is stored in tasks array declared in parent component <ReactTodoApp />. Tasks are rendered in child component <TodoList />. A unique key is assigned to each task in DOM element <label />. When inspecting dev tools unique ids are generating, however error is still present.
Would anyone know why I am still getting the "unique key prop" error?
Link to working application: https://codesandbox.io/s/todo-list-34udn?file=/src/App.js
JS file:
import React, { Component } from "react";
import "./styles.css";
export default class ReactTodoApp extends Component {
constructor(props) {
super(props);
this.state = {
//container for new task
input: "",
tasks: []
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRemove = this.handleRemove.bind(this);
}
handleChange(event) {
this.setState({ input: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
//condition for empty empty
if (!this.state.input) {
return;
}
//declare object to store
const newTask = {
input: this.state.input,
id: 1 + Math.random()
};
//request update to current tasks state
this.setState((state) => ({
tasks: state.tasks.concat(newTask),
input: ""
}));
}
//updater function to remove task
handleRemove(props) {
//create new task list
const newTasksList = this.state.tasks;
//remove selected item from new task list
newTasksList.splice(props, 1);
//update state for tasks
this.setState({ tasks: newTasksList });
}
render() {
return (
<div>
<h1>React Todo</h1>
<form onSubmit={this.handleSubmit} className="add-item">
<input
type="text"
value={this.state.input}
onChange={this.handleChange}
className="add-item__input"
placeholder="new item"
/>
<button type="submit" className="submit">
add item
</button>
</form>
<TodoList tasks={this.state.tasks} handleRemove={this.handleRemove} />
</div>
);
}
}
class TodoList extends React.Component {
render() {
return (
<div className="list-container">
{this.props.tasks.map((task) => (
<label keys={task.id} className="item-container">
<input type="checkbox" />
<p className="item__text">{task.input}</p>
<button onClick={this.props.handleRemove} className="remove-button">
x
</button>
<span className="custom-checkbox" />
</label>
))}
</div>
);
}
}
Just change keys={task.id} to key={task.id}

ReactJS: array of compoents issue

I have a Main.js page that has one button: when you click it it adds a Block component to an array and to the page. You can add as many Block components as you want. Each Block component has a "delete" button, that will remove the Block from the array and from the page.
Menu.js:
import React from 'react';
import './Menu.css';
import Block from './Block.js';
import './Block.css';
export default class Menu extends React.Component {
constructor(props) {
super(props);
this.state = { value: '', blocksArray: [] };
this.addBlock = this.addBlock.bind(this);
this.removeBlock = this.removeBlock.bind(this);
this.blocks = [];
}
addBlock() {
this.blocks.push({ title: 'Section title' + this.blocks.length, content: 'Content' + this.blocks.length });
this.setState({ value: '', blocksArray: this.blocks });
}
removeBlock(index) {
this.blocks.splice(index, 1);
this.setState({ value: '', blocksArray: this.blocks })
}
renderBlocks = () => {
return (
this.state.blocksArray.map((block, index) =>
<Block
remove={() => this.removeBlock(index)}
key={index}
title={block.title}
content={block.content}
/>
)
)
}
render() {
return (
<div>
<div className="Menu">
<header className="Menu-header">
<button className="Menu-button" onClick={ () => this.addBlock() }>Add block</button>
</header>
</div>
<div>
{ this.renderBlocks() }
</div>
</div>
);
}
}
Block.js (version 1)
import React from 'react';
import './Block.css';
class Block extends React.Component {
constructor(props) {
super(props);
this.state = {
title: props.title,
content: props.content,
remove: props.remove
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handleSubmit(event) {
//alert('A name was submitted: ' + this.state.title);
event.preventDefault();
}
render() {
return (
<div className="Block-container">
<form onSubmit={this.handleSubmit}>
<div className="Block-title">
<label>
Title:
<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
</label>
</div>
<div className="Block-content">
<label>
Content:
<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
</label>
</div>
<input type="submit" value="Save" />
<input type="button" value="Delete" onClick= { () => this.state.remove() } />
</form>
</div>
);
}
}
export default Block;
The issue: I found myself stuck with 2 situations and neither works properly.
First non working solution for Block.js:
<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
If I use value={this.props.content} and value={this.props.title} when I push the delete button on the Block it works but I can't edit the text in the field since its value is always retrieved from the props.
Second non working solution for Block.js:
<input type="text" name="title" value={this.state.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.state.content} onChange={this.handleChange} />
If I use value={this.state.content} and value={this.state.title} I can edit the text fields and when I push the delete button on the Block it removes properly the component from the array, but the text displayed in the fields is wrong (it's as if it's always popping only the last component from the array). Let me explain with a few screenshots.
Let's say I added 4 Block components, as follow:
Then I click on the delete button of the Block with "Section title1" / "Content1", as this screenshot:
It apparently removes the right element in the array, but for some reason I get the wrong text in the component:
Array console.log:
0: Object { title: "Section title0", content: "Content0" }
1: Object { title: "Section title2", content: "Content2" }
2: Object { title: "Section title3", content: "Content3" }
Displayed text:
I'm obviously missing something and I have been stuck for a while. Can someone explain what is wrong?
I think the problem is you are setting index as the key for each Block.
Origin keys are [0, 1, 2, 3]. When you remove Section title1, new render will produce keys [0, 1, 2]. So React assumes that element with keys [0, 1, 2] are not changed and key 3 is removed. So it removed the last one.
Try to use an unique property for the key.
You can read more here: https://reactjs.org/docs/reconciliation.html#keys
Your change handler needs to be operating on the state in the parent component where the title/content are coming from. The values shown in the Blocks are being read from the Menu's state, so while editing the block data changes its own internal state, the values coming from the menu to the block as props are staying the same because the internal state is not being fed back.
You could write a function to edit the arrays in the Menu state in place:
this.editBlock = this.editBlock.bind(this);
...
editBlock(index, newBlock) {
let blocks = Array.from(this.state.blocksArray);
blocks[index] = newBlock;
this.setState({
blocksArray: blocks
})
}
and then pass that to the Block as props and call it when the change event fires:
<Block
remove={() => this.removeBlock(index)}
key={index}
title={block.title}
content={block.content}
index={index}
editBlock={this.editBlock}
/>
handleChange(event) {
this.setState({[event.target.name]: event.target.value}, () => {
this.props.editBlock(this.props.index, { title: this.state.title, content: this.state.content})
});
}
Working demo here.

Passing ref up to parent in react

I have a little app that has an input and based on the search value, displays weather for a particular city. I'm stuck at a certain point though. The idea is that once you search a city, it hides the text input and search button and displays some weather info and another search button to search a new city. My issue is that I want to focus on the search box once I click to search again. I hope that makes sense. I read that the ideal way to do this is with refs. I wired it up like such:
class WeatherForm extends React.Component {
constructor(props) {
super(props);
this.city = React.createRef();
}
componentDidMount() {
this.props.passRefUpward(this.city);
this.city.current.focus();
}
render() {
if (this.props.isOpen) {
return (
<div className={style.weatherForm}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
)
} else {
return (
<div className={style.resetButton}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
);
}
}
}
With this I can pass that ref up to the parent to use in my search by using this.state.myRefs.current.value; It works great, but when I try to reference this.state.myRefs.current in a different function to use .focus(), it returns null.
resetSearch = () => {
console.log(this.state.myRefs.current); // <- returns null
this.setState({
isOpen: !this.state.isOpen,
details: [],
video: []
});
}
Is this because I'm hiding and showing different components based on the search click? I've read numerous posts on SO, but I still can't crack this. Any help is appreciated. I'll include the full code below. To see it in full here is the git repo: https://github.com/DanDeller/tinyWeather/blob/master/src/components/WeatherMain.js
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
recentCities: [],
details: [],
isOpen: true,
myRefs: '',
video: '',
city: ''
};
this.updateInputValue = this.updateInputValue.bind(this);
this.getRefsFromChild = this.getRefsFromChild.bind(this);
this.resetSearch = this.resetSearch.bind(this);
this.getWeather = this.getWeather.bind(this);
}
updateInputValue = (e) => {
...
}
resetSearch = () => {
console.log(this.state.myRefs.current);
this.setState({
isOpen: !this.state.isOpen,
details: [],
video: []
});
}
getWeather = (e) => {
...
}
getRefsFromChild = (childRefs) => {
...
}
render() {
return (
<section className={style.container}>
<div className={style.weatherMain + ' ' + style.bodyText}>
<video key={this.state.video} className={style.video} loop autoPlay muted>
<source src={this.state.video} type="video/mp4">
</source>
Your browser does not support the video tag.
</video>
<div className={style.hold}>
<div className={style.weatherLeft}>
<WeatherForm
updateInputValue={this.updateInputValue}
getWeather={this.getWeather}
passRefUpward={this.getRefsFromChild}
resetSearch={this.resetSearch}
isOpen={this.state.isOpen}
/>
<WeatherList
details={this.state.details}
city={this.state.city}
isOpen={this.state.isOpen}
/>
</div>
<div className={style.weatherRight}>
<Sidebar
recentCities={this.state.recentCities}
/>
</div>
<div className={style.clear}></div>
</div>
</div>
</section>
);
}
}
class WeatherForm extends React.Component {
constructor(props) {
super(props);
this.city = React.createRef();
}
componentDidMount() {
this.props.passRefUpward(this.city);
this.city.current.focus();
}
render() {
if (this.props.isOpen) {
return (
<div className={style.weatherForm}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
)
} else {
return (
<div className={style.resetButton}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
);
}
}
}
export default Weather;
You try to achieve unmounted component from DOM, because of this you can not catch the reference. If you put this code your instead of render function of WeatherForm component, you can catch the reference. Because i just hide it, not remove from DOM.
render() {
return (
<div>
<div className={style.weatherForm}
style={this.props.isOpen ? {visibility:"initial"} :{visibility:"hidden"}}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
<div className={style.resetButton} style={this.props.isOpen ? {visibility:"hidden"} :{visibility:"initial"}}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
</div>
)
}
console.log(this.state.myRefs.current) returns null , because it's a reference to an input dom element which does not exists as currently Weather form is displaying Search another city along with a reset button.
In reset function state changes, which results in change of prop isOpen for WeatherForm component. Now, screen would be displaying the input field along with search button.
After component is updated ComponentDidUpdate lifecycle method is called.
Please add ComponentDidUpdate lifecycle method in WeatherForm and add ,
this.city.current.focus() in the body of method.
There is no need to pass reference of a dom element to the parent element as it is not consider as a good practise.
Edit 1 :-
Need to set input field in focus only if prop ( isOpen ) is true as we will get reference to the input field only if its mounted.
ComponentDidUpdate(){
if(this props.isOpen)
this.city.current.focus
}
Link to Lifecycle method :-
https://reactjs.org/docs/react-component.html#componentdidupdate
Hope this helps,
Cheers !!

setState updating state but not rerender

I have a react function to handle a form submit
handleSubmit(e){
this.setState({size: this.state.value});
this.setState({maze : <Maze size={this.state.value}></Maze>}, () => this.forceUpdate());
console.log('set state finished');
e.preventDefault();
}
in the app class which has the following
render() {
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<label>Size: </label>
<input type="number" min="3" step="1" onChange={this.handleChange} value={this.state.value}></input>
<input type="submit" value="Generate Maze"></input>
</form>
{this.state.maze}
</div>
);
}
Yet the code does not rerender the Maze;
2 things to note
I put an alert in the setState callback and it popped up (meaning, I assume, that setState finished and there should have been a rerender) and passed without a rerender
the force update callback doesn't work
How would I fix this?
Complete code of the Main class
class App extends Component {
constructor(props){
super(props);
this.state = {
size : 20,
value : 20,
showMaze : false
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount(){
this.setState({maze : <Maze size={this.state.size}></Maze>});
}
handleChange(e){
this.setState({value : e.target.value});
}
handleSubmit(e){
this.setState({size: this.state.value});
this.setState({showMaze: false});
this.setState({maze : <Maze size={this.state.value}></Maze>}, () => this.forceUpdate());
this.setState({showMaze: true});
e.preventDefault();
}
render() {
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<label>Size: </label>
<input type="number" min="3" step="1" onChange={this.handleChange} value={this.state.value}></input>
<input type="submit" value="Generate Maze"></input>
</form>
{this.state.showMaze? this.state.maze : null}
</div>
);
}
}
complete code of the Maze class (minus the generation algorithm)
class Maze extends Component{
constructor(props){
super(props);
this.state = {
size : this.props.size,
maze : null
}; //declare the maze null for now
}
componentDidMount(){
this.generateMazev2(this.props.size);
}
generateMazev2 (size){
//snippped
}
render(){
var renderMaze = new Array(); //store the table
//calculate the percentage size of each Cell
var size = 80.0 / this.props.size; //the table should ideally take up 80% of the page
if(this.state.maze !== null){
this.state.maze.forEach(function (row, index, arr){
var newRow = new Array();
row.forEach(function (box, theIndex, arr){
newRow[theIndex] = <Cell size={size} top={box.top} bottom={box.bottom} right={box.right} left={box.left}></Cell>;
});
renderMaze[index] = <tr>{newRow}</tr>;
});
return (<table>{renderMaze}</table>);
}else{
return (<p>Generating</p>);
}
}
}
This is not the correct way, UI component should not be stored in state value. State should contain only the data.
Solution:
Maintain a bool in state variable and use that bool for conditional rendering of Maze component, initial value of bool will be false and update the bool inside handleSubmit function.
Like this:
handleSubmit(e){
e.preventDefault();
this.setState({size: this.state.value, showMazeCom: true});
}
render() {
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<label>Size: </label>
<input type="number" min="3" step="1" onChange={this.handleChange} value={this.state.value}></input>
<input type="submit" value="Generate Maze"></input>
</form>
{this.state.showMazeCom && <Maze size={this.state.value}></Maze>}
</div>
);
}
Update:
To avoid the unnecessary re-rending of Maze component, use shouldComponentUpdate lifecycle method, compare the new and old props values, if they are not same then only allow the re-rendering.
Like this:
shouldComponentUpdate(nextProps){
if(nextProps.size != this.props.size)
return true;
return false;
}
One thing you could do is changing the render() method as below.
render() {
return (
<div className="App">
<form onSubmit={this.handleSubmit}>
<label>Size: </label>
<input type="number" min="3" step="1" onChange={this.handleChange} value={this.state.value}></input>
<input type="submit" value="Generate Maze"></input>
</form>
{this.state.showMaze? <Maze size={this.state.value}></Maze> : null}
</div>
);
}
After that modify handleSubmit() method as below
handleSubmit(e){
this.setState({size: this.state.value, showMaze: true});
console.log('set state finished');
e.preventDefault();
}
UPDATE
It is not a good practice to keep the components as state properties. Hence, replace the {this.state.showMaze? this.state.maze : null} line in render method as I've shown.
Further, you need to implement componentWillReceiveProps() method in the Maze component and modify its state according to new props.
componentWillReceiveProps(nextProps) {
this.setState({size: nextProps.size})
}

Categories

Resources