ReactJS - Elegant solution to toggle show element with ref - reactjs

If this.state.editMode is false then the input doesn't show, in which case the ref setup for this.textInput is undefined until the input shows. However if I want to click on the span to show the input I need to focus on the input.
Does anybody have an elegant solution for this or am I overthinking? Thank you in advance.
focusOnTextInput() {
this.textInput.focus();
}
setEditMode() {
this.editMode = !this.editMode;
this.setState(() => ({
editMode: this.editMode
}));
if (this.editMode) {
this.focusOnTextInput();
}
}
render() {
return (
<li className="task">
{
this.state.editMode ?
<input
type="text"
ref={(input) => { this.textInput = input; }}
onKeyUp={this.keyUpCheck}
onChange={this.changeValue}
value={this.state.newTitle} /> :
<span onClick={this.setEditMode}>{ this.state.title }</span>
}
<span>{ this.props.done }</span>
<span>{ this.props.inProgress }</span>
<button onClick={this.props.onRemove}>X</button>
</li>
)
}
Okay I added the change regarding the editMode which works perfectly and also the focus now works as ref means something... EXCELLENT, thank you Tony :-)
setEditMode() {
this.setState(() => ({
editMode: !this.state.editMode
}));
}
componentDidUpdate(prevProps, prevState) {
if (this.state.editMode) {
this.focusOnTextInput();
}
}
Now how do I set this as complete and give you full marks..?

If you use componentDidUpdate(prevProps, prevState) you will be able to check if this.state.editMode is now set and call your this.focusOnTextInput()

Related

How to edit a todo in a todo list without hooks and redux

I have been stuck on this for days reading up on tutorials and articles but can not figure this out. Whenever I click on the pencil icon, I want it to edit the current do to. I have 4 components, the form (searchbar where i add todo), the app.js, the todoList, and a todo.js component. I am keeping all the state in the app and state in the form to keep track of the terms I am entering.
I am thinking I would need to create an editTodo method in the app and pass it down as a prop to the list and then the todoItem. Most tutorials or help online uses hooks or redux but I am learning vanilla React first. I am not asking for the answer directly but rather the steps or thought process to implement editing a todo item in the todolist. I am not sure even if my todo app is correct in the places where I am keeping state. I may get slack for asking.. but I do not know what else to do. Here is my code..
class App extends React.Component {
state = {
todos: []
}
addTodo = (todo) => {
const newToDos = [...this.state.todos, todo];
this.setState({
todos: newToDos
});
};
deleteTodo = (id) => {
const updatedTodos = this.state.todos.filter((todo) => {
return todo.id !== id;
});
this.setState({
todos: updatedTodos
});
}
editTodo = (id, newValue) => {
}
render() {
return (
<div className="container">
<div className="row">
<div className="col">
<Form addTodo={this.addTodo} />
</div>
</div>
<div className="row">
<div className="col">
<ToDoList
todos={this.state.todos}
deleteTodo={this.deleteTodo}
editingTodo={this.state.editingTodo}/>
</div>
</div>
</div>
)
}
}
export default App;
const ToDoList = ({todos, deleteTodo, editingTodo}) => {
const renderedList = todos.map((todo, index) => {
return (
<ul className="list-group" key={todo.id}>
<ToDoItem todo={todo} deleteTodo={deleteTodo} editingTodo={editingTodo}/>
</ul>
)
});
return (
<div>
{renderedList}
</div>
)
}
export default ToDoList;
const ToDoItem = ({todo, deleteTodo}) => {
return (
<div>
<li style={{display: 'flex', justifyContent: 'space-between' }} className="list-group-item m-3">
{todo.text}
<span>
<FontAwesomeIcon
icon={faPencilAlt}
style={{ cursor: 'pointer'}}
/>
<FontAwesomeIcon
icon={faTrash}
style={{ marginLeft: '10px', cursor: 'pointer'}}
onClick={ () => deleteTodo(todo.id)}
/>
</span>
</li>
</div>
);
}
export default ToDoItem;
I don't think the form component is relevant here as I am trying to edit a todo item so will not include it here. If I do need to include it, let me know. It may not look like I have tried to implement this functionality, but either I could not find what I was looking for, understand the code, or just do not know how to implement it.
Update:
I added an isEditing field in the form component to my todo items so that maybe it can help me know if an item is being editing or not. I also redid the editTodo method.
class Form extends React.Component {
state = { term: ''};
handleSubmit = (e) => {
e.preventDefault();
this.props.addTodo({
id: shortid.generate(),
text: this.state.term,
isEditing: false
});
this.setState({
term: ''
});
}
editTodo = (id, newValue) => {
const editedTodos = [...this.state.todos].map((todo) => {
if(todo.id === id) {
todo.isEditing = true;
todo.text = newValue;
}
return todo.text;
});
this.setState({
todos: [...this.state.todos, editedTodos]
});
}
I also passed that method down to the todoList and then to the todoItem like so
const ToDoItem = ({todo, deleteTodo, editTodo}) => {
const renderContent = () => {
if(todo.isEditing) {
return <input type='text' />
} else {
return <span>
<FontAwesomeIcon
icon={faPencilAlt}
style={{ cursor: 'pointer'}}
onClick={ () => editTodo(todo.id, 'new value')}
/>
<FontAwesomeIcon
icon={faTrash}
style={{ marginLeft: '10px', cursor: 'pointer'}}
onClick={ () => deleteTodo(todo.id)}
/>
</span>
}
}
return (
<div>
<li style={{display: 'flex', justifyContent: 'space between'}} className="list-group-item m-3">
{{!todo.isEditing ? todo.text : ''}}
{renderContent()}
</li>
</div>
);
}
So whenever I click on the the edit icon, it successfully shows 'new value' but now also adds an extra todo item which is blank. I figured out how to add the input field so that it shows also. I am accepting the answer Brian provided since it was the most helpful in a lot of ways but have not completed the functionality for editing a todo.
am thinking I would need to create an editTodo method in the app and pass it down as a prop to the list and then the todoItem.
This is exactly what you need to do. And yet:
editTodo method has no logic in it.
ToDoList component receives editingTodo method as a prop instead of defined editTodo.
You are indeed passing the editingTodo futher down to ToDoItem but you are not utilising it there const ToDoItem = ({todo, deleteTodo}) => ...
You don't have an onClick listener on the pencil icon, so nothing can happen.
I don't know how you are planning on doing the editing (modal window with a form, or replacing the text with an input field), either way the bottom line is that you need to trigger your pencil onClick listener with () => editTodo(id, newText).
My recommendation would be - address all 5 points above and for now just hardcode the new value, just to test it out: () => editTodo(id, 'updated value!') and check that everything works. You can worry about getting the real value in there as your next step.

React: onClick event.currentTarget.textContent returning onClick of undefined

In my website, it currently shows users a list of movies based on their input.
When user clicks on the title of the movie that is rendered, I want to setState(chosenOne: the movie title they clicked).
Currently, when I click on movie title, it returns an error stating the following:
Uncaught TypeError: Cannot read property 'onClick' of undefined
at onClick (Fav.js:62)
Any way to fix this?
Any help is greatly appreciated.
import React, { Component } from 'react'
import axios from 'axios';
import '../styles/Rec.scss'
export class Fav extends Component {
constructor (props) {
super (props)
this.state = {
inputOne: '',
chosenOne: '',
movies:[],
};
}
onChangeOne = (event) => {
this.setState({
inputOne: event.target.value
},()=>{
if(this.state.inputOne && this.state.inputOne.length > 1) {
this.getInfo()
} else {
}
})
}
onClick = (event) =>{
this.setState({
chosenOne: event.currentTarget.textContent
})
console.log(this.state.chosenOne)
}
onSubmit = (event) => {
event.preventDefault();
}
getInfo = () => {
let url = `https://api.themoviedb.org/3/search/movie?api_key=''&language=en-US&query='${this.state.inputOne}'&page=1&include_adult=false`
axios.get(url)
.then(res => {
if (res.data) {
const movieData = res.data.results.filter(movie => movie.poster_path != null);
this.setState({movies: movieData});
}
console.log(this.state.movies)
})
}
render() {
return (
<div>
<h1>Favorite Movie of All Time</h1>
<form onSubmit={this.onSubmit}>
<input onChange={this.onChangeOne}/>
<div className="rec__container">
{this.state.movies && this.state.movies.slice(0,3).map(function(movie, genre_ids) {
return(
<div className="rec__sample">
<img className="rec__img" src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} alt="movie poster"/>
<p onClick={event => this.onClick(event)}>{movie.title}</p>
</div>
)
})}
</div>
</form>
</div>
)
}
}
export default Fav
I am quite sure the problem is that this in this.onClick is undefined. This happens when it is not bound correctly to the the class.
I would recommend to change the function declared after map to an arrow function.
<div className="rec__container">
{this.state.movies &&
this.state.movies.slice(0, 3).map((movie, genre_ids) => {
return (
<div className="rec__sample">
<img className="rec__img" src={`https://image.tmdb.org/t/p/w500/${movie.poster_path}`} alt="movie poster" />
<p onClick={event => this.onClick(event)}>{movie.title}</p>
</div>
);
})}
</div>;
Also, you are binding onClick function in your constructor as well as using it as an arrow function. Bind does not work on arrow functions.
Either remove this.onClick = this.onClick.bind(this) or convert onClick into a simple function rather than an arrow function.

Ref current is always null when rendering my element for the first time?

So my problem is simple I guess, I want that when I click an element, my input got the focus in, so this is my methods and constructor on my component :
constructor(props) {
super(props);
this.textInput = React.createRef();
this.state = {
searchValue: ""
};
}
activateSearchZone = action => {
this.props.activateSearchZone(action);
console.log(this.textInput);
this.textInput.current.focus();
};
handleSearchZone = event => {
let searchValue = event.target.value;
this.props.searchForUsers(searchValue, { isSearching: true });
setTimeout(() => {
this.props.searchForUsers(searchValue, {
isSearching: false,
searchDone: true
});
}, 1000);
this.setState({
searchValue
});
};
And this is my component :
{this.props.searchList.activated && (
<div className="search-bar__zone">
<FontAwesomeIcon icon={faSearch} size="xs"></FontAwesomeIcon>
<input
placeholder="Search"
onChange={event => this.handleSearchZone(event)}
value={this.state.searchValue}
type="text"
ref={this.textInput}
></input>
<FontAwesomeIcon
icon={faTimesCircle}
onClick={() => this.activateSearchZone(false)}
></FontAwesomeIcon>
</div>
)}
The console log shows that the current value is null, I understand now why, it is because my element is just rendered I think, but I want the focus in my input when clicking.
How can I do that ?
An help would be much appreciated.
You can focus an input element with autofocus attribute. In react, it will be like <input type="text" autoFocus />, this will do the job.
For detailed explanation, please refer the link https://davidwalsh.name/react-autofocus
That's because react doesn't knows about the ref on initial render. You need to use forwardRef. It is HOC that wraps your component and tells react that there is some ref. And it will not render that until it is available. Here is an example:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));

Cannot control state properly

I have 3 components: App, Map and ListPlaces. In ListPlaces component, when a user types something in the input element, I want to change the state(markers's state) in App.js to show only related markers on the map.
Edit: When I edit my typo, the error was disappeared. However, I think the logic is still wrong. Because when I write something in the input element, markers array would be 0 immediately. And of course, all markers are disappeared.
More Explanation:
After componentDidMount, my markers array holds 7 items. And Map component takes this markers array and render markers on the map. However, I need to control my markers from ListPlaces component according to value of input element. So I put this: onChange={e => {this.updateQuery(e.target.value); changeMarkersHandler(e.target.value)}} in onChange attribute of input element. (Omit the this.updateQuery, for now, you can focus on only changeMarkersHandler).
This changeMarkersHandler runs changeMarkers function in App.js, but I don't know why my marker arrays would be 0 immediately while changeMarkers function is working.
Note: I am using react-google-maps and I've omitted some code blocks which aren't related to question.
App.js
class App extends Component {
constructor(props) {
super(props);
this.state = {
places: [],
markers: [],
markerID: -1,
newMarkers: []
};
this.changeMarkers = this.changeMarkers.bind(this);
}
componentDidMount() {
fetch("api_url")
.then(response => response.json())
.then(data => {
this.setState({
places: data.response.venues,
markers: data.response.venues
});
})
.catch(error => {
console.log("Someting went wrong ", error);
});
}
changeMarkers(value) {
const newMarkers = this.state.markers.filter(
place => place.name === value
);
this.setState({
newMarkers : newMarkers,
markers: newMarkers
})
}
render() {
return (
<div className="App">
<Map role="application"
places={this.state.places}
markers={this.state.markers}
openInfoHandler={this.openInfo}
closeInfoHandler={this.closeInfo}
markerID={this.state.markerID}
googleMapURL="url_here" />
<ListPlaces changeMarkersHandler={this.changeMarkers} />
</div>
);
}
}
ListPlaces.js
import React, { Component } from "react";
import escapeRegExp from "escape-string-regexp";
class ListPlaces extends Component {
state = {
searchQuery: ""
};
updateQuery = query => {
this.setState({ searchQuery: query});
};
render() {
const { toggleListHandler, locations, openInfoHandler, changeMarkersHandler} = this.props;
let showLocations;
if (this.state.searchQuery) {
const match = new RegExp(escapeRegExp(this.state.searchQuery), "i");
showLocations = locations.filter(location =>match.test(location.name));
} else {
showLocations = locations;
}
return (
<div>
<aside>
<h2>Restaurants</h2>
<nav>
<div className="search-area">
<input
className="search-input"
type="text"
placeholder="Search Restaurant"
value={this.state.searchQuery}
onChange={e => {this.updateQuery(e.target.value); changeMarkersHandler(e.target.value)}}
/>
</div>
<ul>
{showLocations.map(location => {
return (
<li
key={location.id}
onClick={e =>
openInfoHandler(e, location.id)
}
>
{location.name}
</li>
);
})}
</ul>
</nav>
<p>some text</p>
</aside>
<a
onClick={toggleListHandler}
id="nav-toggle"
className="position"
>
<span />
</a>
</div>
);
}
}
export default ListPlaces;
You have a typo in you constructor.
this.changeMarkers(this.changeMarkers.bind(this));
should be
this.changeMarkers = this.changeMarkers.bind(this);

I need to update state in render() function in reactjs. How?

What are the solutions for this problem?
<Form.Field>
<label> </label>
<MockMutation mutation={DO_LOGIN}>
{(doLogin, { loading, error, data }) => {
if (!loading && data.loggedInState == "LOGGED_IN") {
// this.setState({goodLogin: true})
// I need to update state here to redirect to other page
// How can I do it with out the annoying warning???
}
return (
<Button
primary
className="login-btn full-width"
disabled={loading}
onClick={e => {
console.log("on click btn clicked");
e.preventDefault();
doLogin({
variables: {
employeeId: this.state.employeeId,
password: this.state.password
}
});
}}
>
<span style={loading ? { display: "none" } : {}}>Login</span>
<span style={loading ? {} : { display: "none" }}>Loading...</span>
</Button>
);
}}
</MockMutation>
</Form.Field>
If you are using react-router.v4 you can use Redirect component to do make a redirect.
if (!loading && data.loggedInState == "LOGGED_IN") {
// this.setState({goodLogin: true})
// I need to update state here to redirect to other page
// How can I do it with out the annoying warning???
return <Redirect to="/some/path" />
}
If you don't use react-router-4 then it is fairly easy to implement such component anyway:
class Redirect extends React.Component {
componentDidMount(){
const { history, to } = this.props;
history.push(to);
}
render() {
return null
}
}
export default withRouter(Redirect);

Resources