Managing Focus In React - javascript

I am writing a autocomplete feature using React, and when the user selects an item on the list I want the input to update. My problem is when the input loses focus I want the menu to disappear unless the user selects the in the menu. Currently, showing the menu is based on a property called showDropDown. In the render method I have if showDropDown it builds the menu components. It seems that the render method is being called before the click listeners on the menu items, and is removing them before onClick is called.
handleOnBlur = () => {
this.setState({ showDropDown: false });
};
handleOnFocus = () => {
this.setState({ showDropDown: true });
};
handleRenderSubComponents = (options) => {
...
return options.map((option) => {
...
return (<li key={displayString} className={className}>
<span onClick={() => {this.handleOnItemSelect(option, fieldInput);}} style={{ cursor: 'pointer' }}>
{displayString}
</span>
</li>);
});
};
render() {
...
return (
<div className={className}>
<div>
<input
style={{ position: 'relative' }}
disabled={disabled}
ref={(inputElem) => {this.inputElem = inputElem;}}
valueLink={{ value: this.state.value, requestChange: (value) => this.handleOnChange(value) }}
onBlur={this.handleOnBlur}
onFocus={this.handleOnFocus}
onKeyUp={this.handleOnKeyUp}
/>
</div>
<ul className="dropdown-menu dropdown-menu-justify" style={dropDownStyle} >
{showDropDown && this.handleRenderSubComponents(options)}
</ul>
</div>
);
}
What I need to do is only hide menu if the input loses focus, but the focus is not in the menu

I think this does what you need. The key is that the onMouseDown event on the container is fired before the onBlur on the input. So we set a class variable that can be check in the onBlur and if the container was clicked we force focus back to the input. The setTimeout clears this variable again, so the order of execution will be onMouseDown, onBlur, setTimeout callback.
jsfiddle
class App extends React.Component {
constructor(props) {
super(props);
this.onInputChange = this.onInputChange.bind(this);
this.onInputBlur = this.onInputBlur.bind(this);
this.onContainerMouseDown = this.onContainerMouseDown.bind(this);
this.onOptionClick = this.onOptionClick.bind(this);
this.state = {
input: '',
showList: false
};
this.options = ['Option 1', 'Option 2', 'Option 3', 'Option 4'];
}
componentWillUnmount() {
clearTimeout(this.containerMouseDownTimeout);
}
onInputChange(e) {
this.setState({input: e.target.value, showList: e.target.value.length > 2});
}
onInputBlur() {
const showList = this.clickedInContainer && this.state.input.length > 2;
this.setState({
showList
});
if (showList) {
this.input.getDOMNode().focus();
}
}
onContainerMouseDown() {
this.clickedInContainer = true;
this.containerMouseDownTimeout = setTimeout(() => {
this.clickedInContainer = false;
});
}
onOptionClick(option) {
this.setState({
input: option,
showList: false
})
}
renderList() {
return (
<ol style={{cursor: 'pointer'}}>
{this.options.map((o, i) => <li key={i} onClick={() => this.onOptionClick(o)}>{o}</li>)}
</ol>);
}
render() {
return (
<div onMouseDown={this.onContainerMouseDown}>
<input ref={ref => this.input = ref} value={this.state.input}
onChange={this.onInputChange} onBlur={this.onInputBlur}/>
{this.state.showList && this.renderList()}
</div>
)
}
}
React.render(<App />, document.getElementById('container'));

Related

Is it possible to add Edit functionality to this Todo App made using ReactJS?

I have created a Todo List app successfully to some extent.
My constructor function of TodoList is as follows:
class TodoList extends React.Component {
constructor(props) {
super(props);
this.state = { value: '',
todoList: [{ id: 1, content: "Call Client" },
{ id: 2, content: "Write Log" }] }
this.onChangeValue = this.onChangeValue.bind(this);
this.onAddItem = this.onAddItem.bind(this);
}
The remaining body of TodoList has Add Item functionality, by using two methods onChangeValue and onAddItem
onChangeValue = event => {
this.setState({ value: event.target.value });
};
onAddItem = () => {
if (this.state.value !== '') {
this.setState(state => {
const todoList = state.todoList.concat({ id: state.todoList.length + 1, content: this.state.value});
return {
todoList,
value: '',
};
});
}
};
render() {
const listItems = this.state.todoList.map((todo) =>
<ListItem key={todo.id} id={todo.id} content={todo.content}/>
)
return <>
<ul className="todo-list">
{listItems}
</ul>
{/* <AddItem/> */}
<div className="add-item">
<input type="text" onChange={this.onChangeValue}/>
<button type="submit" onClick={this.onAddItem}>Add Item</button>
</div>
</>
}
}
Delete functionality and Mark as read functionality are created in the ListItem component using methods handleChange and handleDeleteClick.
class ListItem extends React.Component {
constructor(props) {
super(props);
this.state = { done : false, editing : '', deleted : false }
this.handleChange = this.handleChange.bind(this);
this.handleDeleteClick = this.handleDeleteClick.bind(this);
}
handleChange(event) {
this.setState({done: event.target.checked})
}
handleDeleteClick() {
this.setState({ deleted : true })
}
render() {
if (this.state.deleted === false) {
return <li className="list-item">
{/* special class is added to the paragraph to strike the text when marked as done */}
<p className={this.state.done ? 'done' : ''}>{this.props.content}</p>
<ul className="actions">
<li>
<label htmlFor={'item_' + this.props.id}>Mark as done</label>
<input name={'item_' + this.props.id} id={'item_' + this.props.id} type="checkbox" onChange={this.handleChange}/>
</li>
<li>
{/* Edit button is disabled once the task is marked as done */}
{ this.state.done ? <button type="button" disabled>Edit</button> : <button type="button">Edit</button> }
</li>
<li><button type="button" onClick={this.handleDeleteClick}>Delete</button></li>
</ul>
</li>
}
else {
return null;
}
}
}
Now the only thing remaining is the edit functionality which I cannot figure out if it's possible or not.
The source code of my application can be found in this codepen:
https://codepen.io/blenderous/pen/rNeywyZ
We could create an onEditItem method inside the TodoList item and pass this method to each ListItem. This method would receive an id and a newContent values to process the updates.
// TodoList component
...
onEditItem = (id, newContent) => {
const newTodoList = this.state.todoList.map((todo) => {
// return todo.id !== id ? todo : { ...todo, content: newContent }
// not same id? leave as is
if (todo.id !== id) {
return todo;
}
// update content with the newContent value
return { ...todo, content: newContent };
});
this.setState({ todoList: newTodoList });
};
Then on our ListItem, we'll create an handleEditClick method that will handle the click event for our edit button.
// ListItem component
...
handleEditClick() {
const { id, content } = this.props;
// prompt to edit the current content
const newContent = prompt("Edit:", content);
// call the TodoList editTodo passing the id and the new content
// of the current todo
this.props.editTodo(id, newContent);
}
Now we'll use this method on our edit button like so
...
<button
type="button"
disabled={this.state.done} // disabled once the task is marked as done
onClick={this.handleEditClick}
>
Edit
</button>

Adding clicked items to new array in React

I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!

How to toggle css class of a single element in a .map() function in React

I have a .map() function where I'm iterating over an array and rendering elements, like so:
{options.map((option, i) => (
<TachyonsSimpleSelectOption
options={options[i]}
key={i}
onClick={() => this.isSelected(i)}
selected={this.toggleStyles("item")}
/>
I am toggling the state of a selected element like so:
isSelected (i) {
this.setState({ selected: !this.state.selected }, () => { console.log(this.state.selected) })
}
Using a switch statement to change the styles:
toggleStyles(el) {
switch (el) {
case "item":
return this.state.selected ? "bg-light-gray" : "";
break;
}
}
And then passing it in my toggleStyles method as props to the className of the TachyonsSimpleSelectOption Component.
Problem
The class is being toggled for all items in the array, but I only want to target the currently clicked item.
Link to Sandbox.
What am I doing wrong here?
You're using the selected state incorrectly.
In your code, to determine whether it is selected or not, you depends on that state, but you didn't specify which items that is currently selected.
Instead saving a boolean state, you can store which index is currently selected so that only specified item is affected.
This may be a rough answer, but I hope I can give you some ideas.
on your render:
{options.map((option, i) => (
<TachyonsSimpleSelectOption
options={options[i]}
key={i}
onClick={() => this.setState({ selectedItem: i })}
selected={this.determineItemStyle(i)}
/>
))}
on the function that will determine the selected props value:
determineItemStyle(i) {
const isItemSelected = this.state.selectedItem === i;
return isItemSelected ? "bg-light-gray" : "";
}
Hope this answer will give you some eureka moment
You are not telling react which element is toggled. Since the state has just a boolean value selected, it doesn't know which element is selected.
In order to do that, change your isSelected function to :
isSelected (i) {
this.setState({ selected: i }, () => {
console.log(this.state.selected) })
}
Now, the React state knows that the item on index i is selected. Use that to toggle your class now.
In case you want to store multiple selected items, you need to store an array of indices instead of just one index
TachyonsSimpleSelectOption.js:
import React from 'react';
class Option extends React.Component {
render() {
const { selected, name } = this.props;
return(
<h1
onClick={() => this.props.onClick()}
style={{backgroundColor: selected ? 'grey' : 'white'}}
>Hello {name}!</h1>
)
}
}
export default Option;
index.js:
import React from "react";
import { render } from "react-dom";
import TachyonsSimpleSelectOption from "./TachyonsSimpleSelectOption";
const options = ["apple", "pear", "orange"];
const styles = {
selected: "bg-light-gray"
};
class Select extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
selected: []
};
this.handleClick = this.handleClick.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.isSelected = this.isSelected.bind(this);
}
handleBlur() {
this.toggleMenu(close);
}
handleClick(e) {
this.toggleMenu();
}
toggleMenu(close) {
this.setState(
{
open: !this.state.open
},
() => {
this.toggleStyles("menu");
}
);
}
toggleStyles(el, index) {
switch (el) {
case "menu":
return this.state.open ? "db" : "dn";
break;
case "item":
const { selected } = this.state;
return selected.indexOf(index) !== -1;
break;
}
}
isSelected(i) {
let { selected } = this.state;
if (selected.indexOf(i) === -1) {
selected.push(i);
} else {
selected = selected.filter(index => index !== i);
}
this.setState({ selected});
}
render() {
const { options } = this.props;
return (
<div
className="flex flex-column ba"
onBlur={this.handleBlur}
tabIndex={0}
>
<div className="flex-row pa3" onClick={this.handleClick}>
<span className="flex-grow-1 w-50 dib">Title</span>
<span className="flex-grow-1 w-50 dib tr">^</span>
</div>
<div className={this.toggleStyles("menu")}>
{options.map((option, i) => (
<TachyonsSimpleSelectOption
name={options[i]}
key={i}
onClick={() => this.isSelected(i)}
selected={this.toggleStyles("item", i)}
/>
))}
</div>
</div>
);
}
}
render(<Select options={options} />, document.getElementById("root"));
And Link to Sandbox.

Show and Hide specific component in React from a loop

I have a button for each div. And when I press on it, it has to show the div with the same key, and hide the others.
What is the best way to do it ? This is my code
class Main extends Component {
constructor(props) {
super(props);
this.state = {
messages: [
{ message: "message1", key: "1" },
{ message: "message2", key: "2" }
]
};
}
handleClick(message) {
//something to show the specific component and hide the others
}
render() {
let messageNodes = this.state.messages.map(message => {
return (
<Button key={message.key} onClick={e => this.handleClick(message)}>
{message.message}
</Button>
)
});
let messageNodes2 = this.state.messages.map(message => {
return <div key={message.key}>
<p>{message.message}</p>
</div>
});
return <div>
<div>{messageNodes}</div>
<div>{messageNodes2}</div>
</div>
}
}
import React from "react";
import { render } from "react-dom";
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: [
{ message: "message1", id: "1" },
{ message: "message2", id: "2" }
],
openedMessage: false
};
}
handleClick(id) {
const currentmessage = this.state.messages.filter(item => item.id === id);
this.setState({ openedMessage: currentmessage });
}
render() {
let messageNodes = this.state.messages.map(message => {
return (
<button key={message.id} onClick={e => this.handleClick(message.id)}>
{message.message}
</button>
);
});
let messageNodes2 = this.state.messages.map(message => {
return (
<div key={message.key}>
<p>{message.message}</p>
</div>
);
});
const { openedMessage } = this.state;
console.log(openedMessage);
return (
<div>
{openedMessage ? (
<div>
{openedMessage.map(item => (
<div>
{" "}
{item.id} {item.message}{" "}
</div>
))}
</div>
) : (
<div> Not Opened</div>
)}
{!openedMessage && messageNodes}
</div>
);
}
}
render(<Main />, document.getElementById("root"));
The main concept here is this following line of code.
handleClick(id) {
const currentmessage = this.state.messages.filter(item => item.id === id);
this.setState({ openedMessage: currentmessage });
}`
When we map our messageNodes we pass down the messages id. When a message is clicked the id of that message is passed to the handleClick and we filter all the messages that do not contain the id of the clicked message. Then if there is an openedMessage in state we render the message, but at the same time we stop rendering the message nodes, with this logic {!openedMessage && messageNodes}
Something like this. You should keep in state only message key of visible component and in render method you should render only visible component based on the key preserved in state. Since you have array of message objects in state, use it to render only button that matches the key.
class Main extends Component {
constructor(props) {
super(props);
this.state = {
//My array messages: [],
visibleComponentKey: '',
showAll: true
};
handleClick(message) {
//something to show the specific component and hide the others
// preserve in state visible component
this.setState({visibleComponentKey : message.key, showAll: false});
};
render() {
const {visibleComponentKey, showAll} = this.state;
return (
<div>
{!! visibleComponentKey && ! showAll &&
this.state.messages.filter(message => {
return message.key == visibleComponentKey ? <Button onClick={e => this.handleClick(message)}>{message.message}</Button>
) : <div /> })
}
{ !! showAll &&
this.state.messages.map(message => <Button key={message.key} onClick={e => this.handleClick(message)}>{message.message}</Button>)
}
</div>
);
}
}
I haven't tried it but it gives you a basic idea.
I cannot reply to #Omar directly but let me tell you, this is the best code explanation for what i was looking for! Thank you!
Also, to close, I added a handleClose function that set the state back to false. Worked like a charm!
onCloseItem =(event) => {
event.preventDefault();
this.setState({
openedItem: false
});
}

Handle Redux-form events

Not really sure how to do this, quite new in the Redux-Form world. I have a custom component for my inputs with an onFocus event that displays an unordered list and an onBlur event that hides it. I am also attaching an onClick event to the items because I would like to change the value of the input field once I click on a list item but the click event never fires because blur fires first.
Not sure if I am addressing my code properly.
Here is my custom component:
import React, { Component } from 'react'
import pageStyles from '../../../styles.scss'
class MyInput extends Component {
constructor(props) {
super(props);
this.state = {
dropdownIsVisible: false,
dropdownCssClass: 'dropdown'
}
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleClick = this.handleClick.bind(this);
}
handleFocus () {
this.setState({
dropdownIsVisible: true,
dropdownCssClass: ['dropdown', pageStyles.dropdown].join(' ')
})
}
handleBlur () {
this.setState({
dropdownIsVisible: false,
dropdownCssClass: 'dropdown'
})
}
handleClick () {
console.log('here I am')
}
render() {
const {
input: {
name,
value,
onFocus
},
label,
hasOptions,
options,
cssClass,
meta: {
touched,
error
}
} = this.props
if(options) {
var listItems = options.map((item) =>
<li key={ item.id } onClick={ this.handleClick }>{ item.name }</li>
)
}
return (
<div className={ cssClass }>
<label>{ label }</label>
<input name={ name } id={ name } type="text" onFocus={ this.handleFocus } onBlur={ this.handleBlur } autoComplete="off" />
{ hasOptions === true ? <ul className={ this.state.dropdownCssClass }>{ listItems }</ul> : null }
{ touched && error && <span className="error">{ error }</span> }
</div>
)
}
}
export default MyInput
Use lodash debounce function. This function adds delay before function calling, so you can cancel it in case of nested element click.
Add in constructor:
this.handleBlur = debounce(this.handleBlur, 100);
replace handleClick:
handleClick () {
if(this.handleBlur.cancel){
this.handleBlur.cancel()
}
console.log('here I am')
}

Categories

Resources