I have a component of basic editable list items that operates as such:
My problem is that the cursor does not move into the input box onChange, making a hassle for the user to always click twice. I tried https://coderwall.com/p/0iz_zq/how-to-put-focus-at-the-end-of-an-input-with-react-js but it did not work. Component looks like:
import React from 'react';
import MyComponent from '../utils/MyComponent';
export default class BasicList extends MyComponent {
constructor(props) {
let custom_methods = ['renderItemOrEditField', 'toggleEditing', 'moveCaretAtEnd'];
super(props, custom_methods);
this.state = {editing: null};
}
moveCaretAtEnd(e) {
var temp_value = e.target.value
e.target.value = ''
e.target.value = temp_value
}
renderItemOrEditField(item) {
console.log(item);
if (this.state.editing === item.id) {
return (
<input
onKeyDown={ this.handleEditField }
type="text"
className="form-control"
ref={ `${item.type}_name_${ item.id }` }
name="title"
autofocus
onFocus={this.moveCaretAtEnd}
defaultValue={ item.name }
/>
);
} else {
return (
<li
onClick={this.toggleEditing.bind(null, item.id)}
key={item.id}
className="list-group-item">
{item.name}
</li>
);
}
}
toggleEditing(item_id) {
this.setState({editing: item_id});
}
render() {
let li_elements = null;
let items = this.props.items;
if (items.length > 0) {
li_elements = items.map((item) => {
return (
this.renderItemOrEditField(item)
// {/* }<li key={item.id}>
// {item.name} -
// <button onClick={() => {this.props.deleteCallback(this.props.item_type, item.id, item.name)} }>
// Delete
// </button>
// </li> */}
);
});
}
return (
<div>
<h4>{this.props.title}:</h4>
<ul className="list-group">
{li_elements}
</ul>
</div>
);
}
}
The items I'm working with now only have a name and ID (type denotes a 'role' or 'task')
How can I make the cursor start at the end of the input box text on change?
Autofocus triggers when a component is mounted. Not sure that's happening by your conditional render. You could moving your conditional logic to a separate container and that should trigger a mount each time it's shown.
I ended up setting focus as in the callback that causes the input box to show:
toggleEditing(item_id) {
// this syntax runs the function after this.setState is finished
this.setState({editing: item_id}, function() {
this.textInput.focus();
});
}
Then the original solution given worked:
// https://coderwall.com/p/0iz_zq/how-to-put-focus-at-the-end-of-an-input-with-react-js
moveCaretAtEnd(e) {
var temp_value = e.target.value
e.target.value = ''
e.target.value = temp_value
}
and
<input
onKeyDown={ this.handleEditField }
type="text"
className="form-control"
ref={(input) => { this.textInput = input; }}
name="title"
autofocus
onFocus={this.moveCaretAtEnd}
defaultValue={ item.name }
onChange={(event) => this.editItem(event)}
style={ {maxWidth: 500} }
/>
Related
import React, { Component, createElement } from "react";
export default class TodoList extends Component {
state = {
todo: [],
inputValue: "",
};
addTodo = () => {
this.setState({ todo: [...this.state.todo, this.state.inputValue] });
// I want to insert separate paragraph tags (todos from this.state.todo) into the list element here
};
handleKeyDown = (e) => {
if (e.keyCode === 13) {
if (this.state.inputValue === "") return;
this.addTodo();
}
};
handleChange = (e) => {
this.setState({ inputValue: e.target.value });
};
render() {
return (
<div className="list"></div>
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
);
}
}
I am creating a Todo List where the user types into an input, and the todo is then inserted into the div with class list element. I'm new to React so I don't know the best way I should go about doing this.
Any help is appreciated, thanks!
You can map the array, inside the .list div, and render each todo item, by wrapping it in p tag. I have added a button element, to handle the addTodo() function.
Also, you may want to move the .list div, below the input.
import React, { Component, createElement } from "react";
export default class TodoList extends Component {
state = {
todo: [],
inputValue: ""
};
addTodo = () => {
this.setState({ todo: [...this.state.todo, this.state.inputValue] });
// I want to insert separate paragraph tags (todos from this.state.todo) into the list element here
};
handleKeyDown = (e) => {
if (e.keyCode === 13) {
if (this.state.inputValue === "") return;
this.addTodo();
}
};
handleChange = (e) => {
this.setState({ inputValue: e.target.value });
};
render() {
return (
<div>
<div className="list">
{this.state.todo.map((todo) => {
return <p>{todo}</p>;
})}
</div>
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
<button onClick={this.addTodo}>Add Todo</button>
</div>
);
}
}
Codesandbox link - https://codesandbox.io/s/busy-pascal-txh55?file=/src/App.js
class TodoList extends Component {
state = {
todo: [],
inputValue: "",
};
addTodo = () => {
// I want to insert separate paragraph tags (todos from this.state.todo) into the list element here
// Hint: when you want to add a todo, you simply set input value to empty here.
this.setState({
todo: [...this.state.todo, this.state.inputValue],
inputValue: "",
});
};
handleKeyDown = (e) => {
if (e.keyCode === 13) {
if (this.state.inputValue === "") return;
this.addTodo();
}
};
handleChange = (e) => {
// Hint: I prefer adding "...this.state" every time before updating.
this.setState({ ...this.state, inputValue: e.target.value });
};
render() {
return (
<>
{
// Hint: use React fragment ("<> ... </>") when there's
more than one element in the first level.
}
<div className="list">
{
// Hint: Adding the current list with map in here
}
<ul>
{this.state.todo.map((t, i) => (
<li key={i}>{t}</li>
))}
</ul>
</div>
{
// I would prefer adding it inside a form element and instead of onKeyDown, use onSubmit on the form
// (on enter it will submit automatically, but you will have to do an e.preventDefault() to not refresh the page).
}
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
</>
);
}
}
This is a working example with a few comments. Also setState runs asyncrounously so it's not a good idea to run multiple one at the same time. Hope this helps.
Using map like TechySharnav mentioned is a quick way of doing it. But if you need to do some more complex operations/layout stuff, then writing a custom function and calling it in the render jsx might be cleaner. So, you could have a function like:
renderItems() {
var rows = []
this.state.todo.forEach((elem, idx) => {
// for example
rows.push(
<p>{elem}</p>
)
});
return rows;
}
Then call it inside render:
//...
<div className="list">
{this.renderItems()}
</div>
//...
js map will certainly solve the problem.
this small snippet for printing the list,
render() {
return (
<div className="list">
{ this.state.todo.map((item) => {
return <p>{item}</p>
})}
</div>
<input
type="text"
className="insertTodo"
placeholder="Add a new todo!"
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
value={this.state.inputValue}
/>
);
}
I'm working on the edit/update the prices in the price list in my React app. I'm almost close but was unable to input any changes. Every time I tried to change the price in the input, I get an error saying "TypeError: onChange is not a function"...
I'm trying to write my code that's almost similar to this tutorial: https://medium.com/the-andela-way/handling-user-input-in-react-crud-1396e51a70bf#8858
So far, I was able to toggle the price between input field and back but I'm trying to edit the input to save any changes...What am I missing? I kept checking between my code and this tutorial to make sure everything is working...
Here's my code (functions) in the Parent component, PriceForm.js:
toggleItemEditing = index => {
this.setState({
priceArr: this.state.priceArr.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
isEditing: !item.isEditing
}
}
return item;
})
});
};
handlePriceUpdate = (event, index) => {
const target = event.target;
const value = target.value;
const number = target.number;
this.setState({
priceArr: this.state.priceArr.map((item, itemIndex) => {
if (itemIndex === index) {
return {
...item,
[number]: value
}
}
return item;
})
});
console.log("price update", event);
};
and where it's called in:
{this.state.priceArr.map((props, index) => (
<PriceBox
{...props}
key={props.date}
toggleEditing={this.toggleItemEditing}
handleDeletePrice={this.handleDeletePrice}
onChange={this.handlePriceUpdate}
/>
))}
And here's my code for the Child component, SinglePriceBox.js:
import React, { Component } from "react";
export default class SinglePricebox extends Component {
constructor(props) {
super(props);
this.state = {
isInEditMode: false,
todaydate: this.props.date
};
this.toggleEditPriceSubmission = this.toggleEditPriceSubmission.bind(this);
}
toggleEditPriceSubmission() {
this.setState(state => ({ isInEditMode: !state.isInEditMode }));
}
render() {
const { isInEditMode, onChange, index } = this.state;
return (
<div className="pricebox">
<article className="pricetable">
<table>
<tbody>
<tr>
<td className="date-width">{this.props.date}</td>
<td className="price-width">
{isInEditMode ? (
<input type="text" name="number" value={this.props.number} onChange={event => onChange(event, index)} />
) : (
this.props.number
)}
</td>
<td className="editing-btn">
<button
type="button"
className="edit-btn"
onClick={this.toggleEditPriceSubmission}
>
{isInEditMode ? "Save" : "Edit"}
</button>
</td>
<td>
{this.props.handleDeletePrice && (
<button
type="button"
className="delete-btn"
onClick={() => this.props.handleDeletePrice(this.props.date)}
>
X
</button>
)}
</td>
</tr>
</tbody>
</table>
</article>
</div>
);
}
}
You can check out my demo at https://codesandbox.io/s/github/kikidesignnet/caissa. You will be able to check out the error if you click on Prices button, then click on Edit button to change the price in the input field that appears.
import React, { Component } from "react";
export default class SinglePricebox extends Component {
constructor(props) {
super(props);
this.state = {
isInEditMode: false,
todaydate: this.props.date
};
this.toggleEditPriceSubmission = this.toggleEditPriceSubmission.bind(this);
this.handleChange = this.handleChange.bind(this);
}
toggleEditPriceSubmission() {
this.setState(state => ({ isInEditMode: !state.isInEditMode }));
}
handleChange = (e, index) => {
// write your code here
}
render() {
const { isInEditMode, index } = this.state;
return (
<div className="pricebox">
<article className="pricetable">
<table>
<tbody>
<tr>
<td className="date-width">{this.props.date}</td>
<td className="price-width">
{isInEditMode ? (
<input type="text" name="number" value={this.props.number} onChange={event => this.handleChange(event, index)} />
) : (
this.props.number
)}
</td>
<td className="editing-btn">
<button
type="button"
className="edit-btn"
onClick={this.toggleEditPriceSubmission}
>
{isInEditMode ? "Save" : "Edit"}
</button>
</td>
<td>
{this.props.handleDeletePrice && (
<button
type="button"
className="delete-btn"
onClick={() => this.props.handleDeletePrice(this.props.date)}
>
X
</button>
)}
</td>
</tr>
</tbody>
</table>
</article>
</div>
);
}
}
In the following line:
<input type="text" name="number" value={this.props.number} onChange={event => onChange(event, index)} />
You're calling this.state.onChange but there is no onChange in your state:
this.state = {
isInEditMode: false,
todaydate: this.props.date
};
After looking at your codesandbox, it seems that onChange is passed as a props to PriceBox, so you should do this in SinglePriceBox render():
const { isInEditMode, index } = this.state;
const { onChange } = this.props;
This will remove the error you were having, but the update still doesn't work because target.number is undefined in PriceForm.handlePriceUpdate :(
However target.name is defined and equal to 'number' which is a valid key in your price list
the problem is in how you pass your function as a prop. You should pass the function call in this way:
<PriceBox
{...props}
[...]
onChange={(e) => this.handlePriceUpdate(e, index)}
/>
Then call it in your child component:
<input
type="text"
[...]
onChange={event => this.props.onChange(event, index)}
/>
Also, I would not using index here, I would rather use an ID from your object instead
I've created a React component that takes inputs from other components to display text of various size in a view. Since it is basically a form, what I want to do is pass the current view into another page where I will then post that view to my database as JSON.
Since the state of the input fields are not set in this component, I'm not sure how I would pass them as props to a new view.
This is a condensed version of what my data input component looks like:
INPUTSHOW.JSX
export default class InputShow extends Component {
componentDidMount() {
autosize(this.textarea);
}
render() {
const { node } = this.props;
...
return (
<div className="editor-div" >
{
(node.type === 'buttonA') ?
<textarea
style={hlArea}
ref={a => (this.textarea = a)}
placeholder="type some text"
rows={1}
defaultValue=""
id={node.id} className='editor-input-hl' type="text" onChange={this.props.inputContentHandler} />
:
(node.type === 'buttonB')
?
<textarea
style={subArea}
ref={b => (this.textarea = b)}
placeholder="type some text"
rows={1}
defaultValue=""
id={node.id} className='editor-input-sub' type="text" onChange={this.props.inputContentHandler} />
:
""
}
</div >
)
}
}
This works fine in creating inputs in a current view. I then pass those values to TextAreaField.JSX
export default (props) => {
return (
<>
<button><Link to={{
pathname: '/edit/preview',
text: props.inputsArray
}}>preview</Link></button>
<div className='view'>
{
props.inputsArray.map(
(node, key) => <InputShow key={key} node={node} inputContentHandler={props.inputContentHandler} />
)
}
</div>
</>
)
}
and then finally that is rendered in my Edit.JSX form:
export default class Edit extends React.Component {
constructor(props) {
super(props)
UniqueID.enableUniqueIds(this);
this.state = {
inputs: [],
text: ''
}
}
...
createPage = async () => {
await this.props.postPage(this.state.text)
}
// Handler for listen from button.
buttonCheck = (e) => {
index++;
const node = {
id: this.nextUniqueId() + index,
type: e.target.id,
text: '',
image: true
}
this.setState(
prev => ({
inputs: [...prev.inputs, node]
})
)
console.log(this.state.inputs);
}
inputContentHandler = (e) => {
let newArray = this.state.inputs;
let newNode = newArray.find((node) => {
return (node.id === e.target.id)
})
newNode.text = e.target.value;
this.setState({ inputs: newArray });
console.log(this.state.inputs);
}
render() {
return (
<div>
<InnerHeader />
<div className='some-page-wrapper'>
<div className='row'>
<div className="dash-card-sm">
<br />
<EditButtonContainer buttonCheck={this.buttonCheck} />
<Route path='/edit/form' render={() => (
<TextAreaField
inputsArray={this.state.inputs}
inputContentHandler={this.inputContentHandler}
/>
)}
/>
<Route path='/edit/preview' render={(props) => (
<Preview
inputs={this.state.inputs}
text={this.state.text}
createPage={this.createPage}
/>
)}
/>
<br /> <br />
{/* Button Header */}
</div>
</div>
</div>
</div>
)
}
}
The problem is that I don't know how I should be passing the rendered view to the Preview.jsxcomponent. I'm still new to react (4 months)...Any help in pointing me in the right direction would be appreciated.
I have a list with opening_hours that I map inside my render function. I map them to a child component. I want to change the value of each object inside my parent state when the child component state is changed. Only problem is that my values don't change in the child component nor the parent state. I have passed the onChange method to each child component, but the problem lies in my onChange function in the parent state.
I have tried to pass the onChange to my child component and update the state with [e.target.name] : e.target.value. But that didn't work. So I tried to copy the arrayList and update the state based on the index of the mapped arrayList. That also didn't work.
handleOnChange = (index) => {
let newData = [...this.state.opening_hours];
this.setState(
newData.map((barbershop, j) => {
if (j === index) {
return barbershop;
console.log(barbershop)
}
return {
newData,
};
}));
console.log(newData)
};
render() {
return (
<>
<div className="container mb-2">
<h4>Opening hours </h4>
<Form>
{this.state.opening_hours.map((barbershop, index) => (
<Day
key={barbershop.id}
day={barbershop.day}
open_time={barbershop.open_time}
close_time={barbershop.close_time}
index = {index}
onChange={() => this.handleOnChange(index)}/>
))}
</Form>
<Button onClick={this.handleSubmit} variant="primary" type="submit">
Update opening hours
</Button>
</div>
</>
);
}
}
My child component looks like this
class Day extends Component {
constructor(props) {
super(props);
this.state = {
day: this.props.day,
open_time: this.props.open_time,
close_time: this.props.close_time,
};
console.log(this.state)
}
handleChange = () => {
this.props.onChange(this.props.index)
}
render() {
return (
<Form.Group as={Row}>
<Form.Label column sm="3">
{ENUM[this.state.day]}
</Form.Label>
<Col sm="3">
<Form.Control
name="open_time"
value={this.state.open_time}
onChange={this.handleChange}
type="time"
min="08:00"
max="24:00"/>
</Col>
<Col sm="3">
<Form.Control
name="close_time"
value={this.state.close_time}
onChange={this.handleChange}
type="time"
min="08:00"
max="24:00"/>
</Col>
</Form.Group>
);
}
}
EDIT:
So I have done some investigation, I believe the problem is that you are not returning data back to the consumer.
Here is what I think, the solution is:
import React, { Component, useState, useEffect } from "react";
class UI extends Component {
handleOnChange = ({ index, ...newHours }) => {
// update array item
const opening_hours = this.state.opening_hours.map((item, i) => {
const newData = i === index ? newHours : {};
return {
...item,
...newData
};
});
// update item in opening_hours => state
this.setState({ opening_hours });
};
render() {
return (
<div className="container mb-2">
<h4>Opening hours</h4>
<Form>
{this.state.opening_hours.map(({ id, ...barbershop }, index) => (
<Day
key={id}
{...barbershop}
{...{ index }}
onChange={this.handleOnChange}
/>
))}
</Form>
<Button onClick={this.handleSubmit} variant="primary" type="submit">
Update opening hours
</Button>
</div>
);
}
}
const Day = ({ day, onChange, index, ...props }) => {
const [open_time, setOpenTime] = useState(props.open_time);
const [close_time, setCloseTime] = useState(props.close_time);
useEffect(() => {
onChange({ index, open_time, close_time });
}, [open_time, close_time, onChange, index]);
const sharedProps = {
type: "time",
min: "08:00",
max: "24:00"
};
return (
<Form.Group as={Row}>
<Form.Label column sm="3">
{ENUM[day]}
</Form.Label>
<Col sm="3">
<Form.Control
{...sharedProps}
name="open_time"
value={open_time}
onChange={({ target }) => setOpenTime(target.value)}
/>
</Col>
<Col sm="3">
<Form.Control
{...sharedProps}
name="close_time"
value={close_time}
onChange={({ target }) => setCloseTime(target.value)}
/>
</Col>
</Form.Group>
);
};
You're passing a function that calls this.handleChange(index), but you should really be passing that method as a prop to your <Day /> component and call it in there. You're already passing in the index (although I would use barbershop.id instead)
So in your parent component, I would do this instead:
<Day
key={barbershop.id}
day={barbershop.day}
open_time={barbershop.open_time}
close_time={barbershop.close_time}
index = {barbershop.id}
handleChange={this.handleOnChange}
/>
Then in your this.handleChange method in the parent component, if you're just trying to get this.state.opening_hours for a specific barbershop, I would do something like this instead:
handleChange = (id) => {
this.setState(() => ({
opening_hours: this.state.opening_hours.filter((barbershop) => {
return barbershop.id === id;
})
}))
}
I render a list of items that are contentEditable. When I switch focus to the second element, the first element is still white. I thought the color switch statement (using this.state.active check) would work but clearly, I'm lacking in my thinking here. How would you go about it? Must I implement the logic in Comments component/container instead?
In parent container comments.tsx, I render the comments with;
<div className="comments">
<div>
{comments.map((value, key) => {
return (
<Comment key={key} profile={this.state.profile} data={value}/>
);
})
}
</div>
</div>
</div>
and in comment.tsx, I have;
interface IProps {
key: number;
profile: IProfile;
data: object;
}
export class Comment extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
this.state = {
editableColor: 'green',
active:false
}
}
editReview = (e, data) => {
let { _id, user, comm } = data;
this.setState({active: true}, function () {
this.state.active ? this.setState({editableColor:'#ffffff'}) : this.setState({editableColor:'green'});
});
}
render() {
let { key, profile, data } = this.props;
return(
<div className="col-8 comment">
<p id="comment" contentEditable style={{backgroundColor: this.state.editableColor}}
onFocus={(e) => this.editReview(e, data)}
>
{data['comm']}
</p>
<button onClick={(e) => this.update(e, data)}>Update</button>
</div>
);
}
}
It seems to me that you never come back from the active state, you should implement an onBlur event handler to revert state.active to false:
...
<p
id="comment"
contentEditable style={{backgroundColor: this.state.editableColor}}
onFocus={(e) => this.editReview(e, data)}
onBlur={ () => this.setState({ active: false }) }
>
{data['comm']}
</p>
...
Was actually easy. As always I should have passed editableColor as a prop to Component in comments.tsx
<Comment key={key} profile={this.state.profile} data={value} editableColor={'green'}/>
Pull it out as a prop in comment.tsx
let { key, profile, data, editableColor } = this.props;
Switch out the color depending on focus/blur
editReview = (e, data) => {
this.setState({active: true});
}
<p
id="comment"
contentEditable
style={{backgroundColor: this.state.active ? editableColor='#ffffff' : editableColor}}
onFocus={(e) => this.editReview(e, data)}
onBlur={ () => this.setState({ active: false }) }
>