Reseting a store object to a default value in SolidJS - javascript

How would you completely reset a value in a store in SolidJS
I have something akin to:
interface Item { id: number, price: number, quantity: number }
​interface State { items: Array<Item> }
export const ItemsContext = createContext()
export const ContextProvider = (props: any) => {
const [state, setState] = createStore({items: []})
const incrementItemQuantity = ({id}: Item) => {
const index = state.items.findIndex(i => i.id === id)
if(index !== -1) {
const item = state.items[index]
setState("items", index, {quantity: item.quantity + 1})
}
}
const clearItems = () => {
setState(produce(s => s.items = []))
}
const addItem = (item: Item) => {
setState(produce(s => s.items.push(item))
}
const value = [state, { addItem, clearItems, incrementItemQuantity} ]
return (
<ItemsContext.Provider value={value} >
{ props.children }
<ItemsContext.Provider/>
)
}
Adding an item and incrementing its quantity works as expected.
When I:
Add an item.
Increment its quantity
Clear the items
I expect the state to be blank. However, If I add an item with the same properties as the first to the list, it is displayed with the old values.
I can't figure out why. What am I not doing right ?

You are not using the store API correctly. For example, the item path gives you the item, you should get the item first, than update it through a setter:
setState("items", index, {quantity: item.quantity + 1});
Here is how you can do it correctly:
// Here path gives us the item
setState("items", index, item => ({...item, quantity: item.quantity + 1}));
// Here path gives us the property
setState('items', index, 'quantity', q => q + 1);
Here is how you can do it. I did not expose store but items. It is up to you.
// #refresh reload
import { createContext, JSX, useContext } from "solid-js";
import { createStore, produce } from 'solid-js/store';
import { render } from "solid-js/web";
interface Item { id: number, price: number, quantity: number }
interface Store {
items: () => Array<Item>;
add?: (item: Item) => void;
increment?: (index: number) => void;
clear?: () => void;
};
export const CartContext = createContext<Store>();
export const CartProvider = (props: { children: JSX.Element }) => {
const [store, setStore] = createStore({ items: [{ id: 0, price: 10, quantity: 1 }] })
const items = () => store.items;
const add = (item: Item) => setStore('items', items => [...items, item]);
const increment = (index: number) => setStore('items', index, 'quantity', q => q + 1);
const clear = () => setStore('items', []);
return (
<CartContext.Provider value={{ items, add, increment, clear }}>
{props.children}
</CartContext.Provider>
);
}
const Child = () => {
const { items, add, increment, clear } = useContext(CartContext);
return (
<div>
<ul>
{items().map((item, index) => (
<li>{JSON.stringify(item)} <button onclick={() => increment(index)}>inc</button></li>)
)}
</ul>
<div>
<button onClick={() => add({ id: items().length, price: 10, quantity: 1 })}>Add Item</button>
{` `}
<button onClick={() => clear()}>Clear Items</button>
</div>
</div>
)
};
const App = () => {
return (
<CartProvider>
<Child />
</CartProvider>
);
}
render(App, document.querySelector("#app"));

You are not using the store correctly. Check this live example here
import { render } from "solid-js/web";
import { createContext, useContext, For } from "solid-js";
import { createStore } from "solid-js/store";
export const CounterContext = createContext([{ items: [] }, {}]);
export function CounterProvider(props) {
const [state, setState] = createStore({ items: props.items || []});
const store = [
state,
{
add: (val) => setState("items", (c) => [...c, val]),
clear: () => setState("items", () => []),
},
];
return (
<CounterContext.Provider value={store}>
{props.children}
</CounterContext.Provider>
);
}
const Counter = () => {
const [state, { add,clear }] = useContext(CounterContext);
return <>
<For each={state.items}>
{(i) => (<h1>{i}</h1>)}
</For>
<button onClick={() => add(state.items.length + 1)}>Add </button>
<button onClick={clear}>Clear </button>
</>
};
const App = () => (
<CounterProvider>
<Counter />
</CounterProvider>
);
render(() => <App />, document.getElementById("app")!);

Related

How can I create an instance of an Object in React..?

I am very new to react and javascript, but I am trying to build a simple ToDo App. It wasn't complicated until I wanted to read data from a file and to display that data on the screen. The problem is that I don't know how to create a new Todo object to pass it as parameter for addTodo function.. Thaaank you all and hope you can help me!!
I will let the code here (please see the -loadFromFile- function, there is the problematic place:
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import Todo from './Todo';
import data from './data/data.json'
function TodoList() {
const [todos, setTodos] = useState([]);
const loadFromFile = data.map( ( data) => {
const newTodo = addTodo(new Todo(data.id,data.text));
return ( {newTodo} )});
const addTodo = todo => {
if (!todo.text || /^\s*$/.test(todo.text)) {
return;
}
const newTodos = [todo, ...todos];
setTodos(newTodos);
console.log(...todos);
};
const updateTodo = (todoId, newValue) => {
if (!newValue.text || /^\s*$/.test(newValue.text)) {
return;
}
setTodos(prev => prev.map(item => (item.id === todoId ? newValue : item)));
};
const removeTodo = id => {
const removedArr = [...todos].filter(todo => todo.id !== id);
setTodos(removedArr);
};
const completeTodo = id => {
let updatedTodos = todos.map(todo => {
if (todo.id === id) {
todo.isComplete = !todo.isComplete;
}
return todo;
});
setTodos(updatedTodos);
};
return (
<>
<TodoForm onSubmit={addTodo} />
{loadFromFile}
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}
export default TodoList;
I want to create new instance of Todo object. I tried many times, many different forms, but still doesn't work. I have an id and a text from the data.json file. I want to create that instance of Todo object with these two values. But how?
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import EditIcon from '#material-ui/icons/Edit';
import DeleteIcon from '#material-ui/icons/Delete';
const Todo = ({ todos, completeTodo, removeTodo, updateTodo }) => {
const [edit, setEdit] = useState({
id: null,
value: ''
});
const submitUpdate = value => {
updateTodo(edit.id, value);
setEdit({
id: null,
value: ''
});
};
if (edit.id) {
return <TodoForm edit={edit} onSubmit={submitUpdate} />;
}
return todos.map((todo, index) => (
<div
className={todo.isComplete ? 'todo-row complete' : 'todo-row'}
key={index}
>
<p> <div key={todo.id} onClick={() => completeTodo(todo.id)}>
{todo.text}
</div>
</p>
<div className='icons'>
<DeleteIcon fontSize="small"
onClick={() => removeTodo(todo.id)}
className='delete-icon'
/>
<EditIcon
onClick={() => setEdit({ id: todo.id, value: todo.text })}
className='edit-icon'
/>
</div>
</div>
));
};
export default Todo;
import React, { useState, useEffect, useRef } from 'react';
import { Fab, IconButton } from "#material-ui/core";
import AddIcon from '#material-ui/icons/Add';
function TodoForm(props) {
const [input, setInput] = useState(props.edit ? props.edit.value : '');
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
});
const handleChange = e => {
setInput(e.target.value);
};
const handleSubmit = e => {
e.preventDefault();
props.onSubmit({
id: Math.floor(Math.random() * 10000),
text: input
});
setInput('');
};
return (
<form onSubmit={handleSubmit} className='todo-form'>
{props.edit ? (
<>
<textarea cols="10"
placeholder='Update item'
value={input}
onChange={handleChange}
name='text'
ref={inputRef}
className='todo-input edit'
/>
<button onClick={handleSubmit} className='todo-button edit'>
Save
</button>
</>
) : (
<>
<input
placeholder='Add item'
value={input}
onChange={handleChange}
name='text'
className='todo-input'
ref={inputRef}
/>
<Fab color="primary" aria-label="add">
< AddIcon onClick={handleSubmit} fontSize="small" />
</Fab>
</>
)}
</form>
);
}
export default TodoForm;
Issue
Ah, I see what you are getting at now, you are wanting to load some list of todos from an external file. The main issue I see in your code is that you are attempting to call/construct a Todo React component manually and this simply isn't how React works. You render data/state/props into JSX and pass this to React and React handles instantiating the components and computing the rendered DOM.
const loadFromFile = data.map((data) => {
const newTodo = addTodo(new Todo(data.id, data.text));
return ({newTodo});
});
Todo shouldn't be invoked directly, React handles this.
Solution
Since it appears the data is already an array of objects with the id and text properties, it conveniently matches what you store in state. You can simply pass data as the initial todos state value.
const [todos, setTodos] = useState(data);
If the data wasn't readily consumable you could create an initialization function to take the data and transform/map it to the object shape your code needs.
const initializeState = () => data.map(item => ({
id: item.itemId,
text: item.dataPayload,
}));
const [todos, setTodos]= useState(initializeState);
Running Example:
import data from "./data.json";
function TodoList() {
const [todos, setTodos] = useState(data); // <-- initial state
const addTodo = (text) => {
if (!text || /^\s*$/.test(text)) {
return;
}
setTodos((todos) => [todo, ...todos]);
};
const updateTodo = (id, newTodo) => {
if (!newTodo.text || /^\s*$/.test(newTodo.text)) {
return;
}
setTodos((todos) => todos.map((todo) => (todo.id === id ? newTodo : todo)));
};
const removeTodo = (id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
};
const completeTodo = (id) => {
setTodos((todos) =>
todos.map((todo) =>
todo.id === id
? {
...todo,
isComplete: !todo.isComplete
}
: todo
)
);
};
return (
<>
<TodoForm onSubmit={addTodo} />
<Todo
todos={todos}
completeTodo={completeTodo}
removeTodo={removeTodo}
updateTodo={updateTodo}
/>
</>
);
}

How to keyboard navigate ListGroup and ListGroupItem [duplicate]

I have build a simple component with a single text input and below of that a list (using semantic ui).
Now I would like to use the arrow keys to navigate through the list.
First of all I have to select the first element. But how do I access a specific list element?
Second I would get the information of the current selected element and select the next element. How do I get the info which element is selected?
Selection would mean to add the class active to the item or is there a better idea for that?
export default class Example extends Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.state = { result: [] }
}
handleChange(event) {
// arrow up/down button should select next/previous list element
}
render() {
return (
<Container>
<Input onChange={ this.handleChange }/>
<List>
{
result.map(i => {
return (
<List.Item key={ i._id } >
<span>{ i.title }</span>
</List.Item>
)
})
}
</List>
</Container>
)
}
}
Try something like this:
export default class Example extends Component {
constructor(props) {
super(props)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.state = {
cursor: 0,
result: []
}
}
handleKeyDown(e) {
const { cursor, result } = this.state
// arrow up/down button should select next/previous list element
if (e.keyCode === 38 && cursor > 0) {
this.setState( prevState => ({
cursor: prevState.cursor - 1
}))
} else if (e.keyCode === 40 && cursor < result.length - 1) {
this.setState( prevState => ({
cursor: prevState.cursor + 1
}))
}
}
render() {
const { cursor } = this.state
return (
<Container>
<Input onKeyDown={ this.handleKeyDown }/>
<List>
{
result.map((item, i) => (
<List.Item
key={ item._id }
className={cursor === i ? 'active' : null}
>
<span>{ item.title }</span>
</List.Item>
))
}
</List>
</Container>
)
}
}
The cursor keeps track of your position in the list, so when the user presses the up or down arrow key you decrement/increment the cursor accordingly. The cursor should coincide with the array indices.
You probably want onKeyDown for watching the arrow keys instead of onChange, so you don't have a delay or mess with your standard input editing behavior.
In your render loop you just check the index against the cursor to see which one is active.
If you are filtering the result set based on the input from the field, you can just reset your cursor to zero anytime you filter the set so you can always keep the behavior consistent.
The accepted answer was very useful to me thanks! I adapted that solution and made a react hooks flavoured version, maybe it will be useful to someone:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useKeyPress = function(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
React.useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, [targetKey]);
return keyPressed;
};
const items = [
{ id: 1, name: "Josh Weir" },
{ id: 2, name: "Sarah Weir" },
{ id: 3, name: "Alicia Weir" },
{ id: 4, name: "Doo Weir" },
{ id: 5, name: "Grooft Weir" }
];
const ListItem = ({ item, active, setSelected, setHovered }) => (
<div
className={`item ${active ? "active" : ""}`}
onClick={() => setSelected(item)}
onMouseEnter={() => setHovered(item)}
onMouseLeave={() => setHovered(undefined)}
>
{item.name}
</div>
);
const ListExample = () => {
const [selected, setSelected] = useState(undefined);
const downPress = useKeyPress("ArrowDown");
const upPress = useKeyPress("ArrowUp");
const enterPress = useKeyPress("Enter");
const [cursor, setCursor] = useState(0);
const [hovered, setHovered] = useState(undefined);
useEffect(() => {
if (items.length && downPress) {
setCursor(prevState =>
prevState < items.length - 1 ? prevState + 1 : prevState
);
}
}, [downPress]);
useEffect(() => {
if (items.length && upPress) {
setCursor(prevState => (prevState > 0 ? prevState - 1 : prevState));
}
}, [upPress]);
useEffect(() => {
if (items.length && enterPress) {
setSelected(items[cursor]);
}
}, [cursor, enterPress]);
useEffect(() => {
if (items.length && hovered) {
setCursor(items.indexOf(hovered));
}
}, [hovered]);
return (
<div>
<p>
<small>
Use up down keys and hit enter to select, or use the mouse
</small>
</p>
<span>Selected: {selected ? selected.name : "none"}</span>
{items.map((item, i) => (
<ListItem
key={item.id}
active={i === cursor}
item={item}
setSelected={setSelected}
setHovered={setHovered}
/>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<ListExample />, rootElement);
Attributing useKeyPress functionality to this post.
Pretty much same solution as what #joshweir provided, but in Typescript. Also instead of 'window' object I used 'ref' and added the event listeners only to the input text box.
import React, { useState, useEffect, Dispatch, SetStateAction, createRef, RefObject } from "react";
const useKeyPress = function (targetKey: string, ref: RefObject<HTMLInputElement>) {
const [keyPressed, setKeyPressed] = useState(false);
const downHandler = ({ key }: { key: string }) => {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = ({ key }: { key: string }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
React.useEffect(() => {
ref.current?.addEventListener("keydown", downHandler);
ref.current?.addEventListener("keyup", upHandler);
return () => {
ref.current?.removeEventListener("keydown", downHandler);
ref.current?.removeEventListener("keyup", upHandler);
};
});
return keyPressed;
};
const items = [
{ id: 1, name: "Josh Weir" },
{ id: 2, name: "Sarah Weir" },
{ id: 3, name: "Alicia Weir" },
{ id: 4, name: "Doo Weir" },
{ id: 5, name: "Grooft Weir" }
];
const i = items[0]
type itemType = { id: number, name: string }
type ListItemType = {
item: itemType
, active: boolean
, setSelected: Dispatch<SetStateAction<SetStateAction<itemType | undefined>>>
, setHovered: Dispatch<SetStateAction<itemType | undefined>>
}
const ListItem = ({ item, active, setSelected, setHovered }: ListItemType) => (
<div
className={`item ${active ? "active" : ""}`}
onClick={() => setSelected(item)}
onMouseEnter={() => setHovered(item)}
onMouseLeave={() => setHovered(undefined)}
>
{item.name}
</div>
);
const ListExample = () => {
const searchBox = createRef<HTMLInputElement>()
const [selected, setSelected] = useState<React.SetStateAction<itemType | undefined>>(undefined);
const downPress = useKeyPress("ArrowDown", searchBox);
const upPress = useKeyPress("ArrowUp", searchBox);
const enterPress = useKeyPress("Enter", searchBox);
const [cursor, setCursor] = useState<number>(0);
const [hovered, setHovered] = useState<itemType | undefined>(undefined);
const [searchItem, setSearchItem] = useState<string>("")
const handelChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setSelected(undefined)
setSearchItem(e.currentTarget.value)
}
useEffect(() => {
if (items.length && downPress) {
setCursor(prevState =>
prevState < items.length - 1 ? prevState + 1 : prevState
);
}
}, [downPress]);
useEffect(() => {
if (items.length && upPress) {
setCursor(prevState => (prevState > 0 ? prevState - 1 : prevState));
}
}, [upPress]);
useEffect(() => {
if (items.length && enterPress || items.length && hovered) {
setSelected(items[cursor]);
}
}, [cursor, enterPress]);
useEffect(() => {
if (items.length && hovered) {
setCursor(items.indexOf(hovered));
}
}, [hovered]);
return (
<div>
<p>
<small>
Use up down keys and hit enter to select, or use the mouse
</small>
</p>
<div>
<input ref={searchBox} type="text" onChange={handelChange} value={selected ? selected.name : searchItem} />
{items.map((item, i) => (
<ListItem
key={item.id}
active={i === cursor}
item={item}
setSelected={setSelected}
setHovered={setHovered}
/>
))}
</div>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<ListExample />, rootElement);
This is my attempt, with the downside that it requires the rendered children to pass ref correctly:
import React, { useRef, useState, cloneElement, Children, isValidElement } from "react";
export const ArrowKeyListManager: React.FC = ({ children }) => {
const [cursor, setCursor] = useState(0)
const items = useRef<HTMLElement[]>([])
const onKeyDown = (e) => {
let newCursor = 0
if (e.key === 'ArrowDown') {
newCursor = Math.min(cursor + 1, items.current.length - 1)
} else if (e.key === 'ArrowUp') {
newCursor = Math.max(0, cursor - 1)
}
setCursor(newCursor)
const node = items.current[newCursor]
node?.focus()
}
return (
<div onKeyDown={onKeyDown} {...props}>
{Children.map(children, (child, index) => {
if (isValidElement(child)) {
return cloneElement(child, {
ref: (n: HTMLElement) => {
items.current[index] = n
},
})
}
})}
</div>
)
}
Usage:
function App() {
return (
<ArrowKeyListManager>
<button onClick={() => alert('first')}>First</button>
<button onClick={() => alert('second')}>Second</button>
<button onClick={() => alert('third')}>third</button>
</ArrowKeyListManager>
);
}
It's a list with children that can be navigated by pressing the left-right & up-down key bindings.
Recipe.
Create an Array of Objects that will be used as a list using a map function on the data.
Create a useEffect and add an Eventlistener to listen for keydown actions in the window.
Create handleKeyDown function in order to configure the navigation behaviour by tracking the key that was pressed, use their keycodes fo that.
keyup: e.keyCode === 38
keydown: e.keyCode === 40
keyright: e.keyCode === 39
keyleft: e.keyCode === 37
Add State
let [activeMainMenu, setActiveMainMenu] = useState(-1);
let [activeSubMenu, setActiveSubMenu] = useState(-1);
Render by Mapping through the Array of objects
<ul ref={WrapperRef}>
{navigationItems.map((navigationItem, Mainindex) => {
return (
<li key={Mainindex}>
{activeMainMenu === Mainindex
? "active"
: navigationItem.navigationCategory}
<ul>
{navigationItem.navigationSubCategories &&
navigationItem.navigationSubCategories.map(
(navigationSubcategory, index) => {
return (
<li key={index}>
{activeSubMenu === index
? "active"
: navigationSubcategory.subCategory}
</li>
);
}
)}
</ul>
</li>
);
})}
</ul>
Find the above solution in the following link:
https://codesandbox.io/s/nested-list-accessible-with-keys-9pm3i1?file=/src/App.js:2811-3796

Map function increment on global react context state

I need some help with incrementing a value through map function while using React's context API. Here is an example to better understand:
Let's say I have items:
const [items, setItems] = useContext(ItemsContext)
These items are JSON objects inside an array.
And then I want to return each item's properties in a list but some of them modified - for example, the item has quantity and I want to increment/decrement it on click. How do I achieve this individually for every item?
I tried making a local state for the quantities:
const [quantity, setQuantity] = useState([])
,so I have all the quantities of all elements but it got me nowhere.
The thing I am trying to accomplish is similar to this:
<div>
<ul>
{
items.map(item => (
<li>
<p>item.name</p>
<p>item.quantity</p>
<button onClick={incQuantity}> </button>
</li>
}
</ul>
</div>
Edit:
const [idCounter, setIdCounter] = useState(0)
I use props. here because this is another component.
const addItem = () => {
if (quantity > 0) {
setIdCounter(idCounter + 1)
setItems(prevItems => [...prevItems, {id: idCounter, name: props.name, price: props.price, quantity: quantity }])
}
}
And I implemented the handler quite the same:
const quantityHandler = (id, diff) => {
setItems(items.map((item) =>
item.id === id ? {...item, quantity: item.quantity + diff} : item
))
}
And here is the list itself:
<div>
<ul>
{
items.map(item => (
<li>
<p>item.name</p>
<p>item.quantity</p>
<button onClick={() => quantityHandler(item.id, 1)}> </button>
<button onClick={() => quantityHandler(item.id, -1)}> </button>
</li>
}
</ul>
</div>
Here is working example and I will explain it a little: in App we make MyContext and state with hook, then we provide state and function to update state to Context provider as value. Then in any place inside Provider we have access to that state and setter. We render items and we can update them using hook setter from Context.
import React, { useState, useContext } from "react";
const MyContext = React.createContext(null);
const initialState = [
{ id: 1, quantity: 1 },
{ id: 2, quantity: 2 },
{ id: 3, quantity: 3 },
{ id: 4, quantity: 4 },
];
const DeepNestedComponent = () => {
const [stateFromContext, setStateFromContext] = useContext(MyContext);
// MyContext should be imported
const buttonHandler = (id, diff) => {
setStateFromContext(
stateFromContext.map((item) =>
item.id === id ? { ...item, quantity: item.quantity + diff } : item
)
);
};
return (
<div>
{stateFromContext.map(({ id, quantity }) => (
<div key={id}>
{quantity}
<button onClick={() => buttonHandler(id, 1)}> + </button>
<button onClick={() => buttonHandler(id, -1)}> - </button>
</div>
))}
</div>
);
};
const App = () => {
const [contextState, setContextState] = useState(initialState);
return (
<MyContext.Provider value={[contextState, setContextState]}>
<DeepNestedComponent />
</MyContext.Provider>
);
};
export default App;
Like it if its is working )

I am trying to do this question but, state is giving me problems

You have a GroceryApp component, which receives a list of products, each one with name and votes. The app should render an unordered list, with a list item for each product. Products can be upvoted or downvoted.
By appropriately using React state and props, implement the upvote/downvote logic. Keep the state in the topmost component, while the Product component should accept props.
const Product = props => {
const {name,votes} = props.product
const plus = () => {
// Call props.onVote to increase the vote count for this product
props.onVote(1,props.index);
};
const minus = () => {
// Call props.onVote to decrease the vote count for this product
props.onVote(-1,props.index);
};
return (
<li>
<span>{name}</span> - <span>votes: {votes}</span>
<button onClick={plus}>+</button>{" "}
<button onClick={minus}>-</button>
</li>
);
};
const GroceryApp = (props) => {
var [products, setProducts] = React.useState(props.products);
const onVote = (dir, index) => {};
return (
<ul>
{products.map((product,index) => (
<Product product={product} index={index} onVote={onVote} />
))}
{/* Render an array of products, which should call onVote when + or - is clicked */}
</ul>
);
}
document.body.innerHTML = "<div id='root'></div>";
ReactDOM.render(
<GroceryApp
products={[
{ name: "Oranges", votes: 0 },
{ name: "Bananas", votes: 0 }
]}
/>, document.getElementById('root')
);
let plusButton = document.querySelector("ul > li > button");
if (plusButton) {
plusButton.click();
}
console.log(document.getElementById('root').outerHTML)
You can update your products and sort them during rendering (can be costly if your list is very large).
const GroceryApp = (props) => {
const [products, setProducts] = React.useState(props.products);
const onVote = (dir, index) => {
const nextProducts = [...products];
const product = products[index];
nextProducts[index] = { ...product, votes: product.votes + dir };
setProducts(nextProducts)
};
return (
<ul>
{products
.sort((a, b) => b.votes - a.votes) // descending sort
.map((product,index) => (
<Product product={product} index={index} onVote={onVote} />
))
}
</ul>
);
}
This worked for me
import React from "react";
const Product = (props) => {
const plus = (ind) => {
props.onVote("+", ind);
// console.log({ ind });
};
const minus = (ind) => {
props.onVote("-", ind);
// console.log({ ind });
};
return (
<li>
<span>{props.product.name}</span> -{" "}
<span>votes: {props.product.votes}</span>
<button onClick={() => plus(props.index)}>+</button>{" "}
<button onClick={() => minus(props.index)}>-</button>
</li>
);
};
const GroceryApp = () => {
let [products, setProducts] = React.useState([
{ name: "Oranges", votes: 0 },
{ name: "Bananas", votes: 0 },
]);
const onVote = (dir, index) => {
const Products = [...products];
if (dir === "+") {
Products[index].votes = Products[index].votes + 1;
setProducts(Products);
} else {
Products[index].votes = Products[index].votes - 1;
setProducts(Products);
}
};
return (
<ul>
{products.map((product, index) => (
<Product key={index} onVote={onVote} index={index} product={product} />
))}
</ul>
);
};
export default GroceryApp;

React: How to navigate through list by arrow keys

I have build a simple component with a single text input and below of that a list (using semantic ui).
Now I would like to use the arrow keys to navigate through the list.
First of all I have to select the first element. But how do I access a specific list element?
Second I would get the information of the current selected element and select the next element. How do I get the info which element is selected?
Selection would mean to add the class active to the item or is there a better idea for that?
export default class Example extends Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.state = { result: [] }
}
handleChange(event) {
// arrow up/down button should select next/previous list element
}
render() {
return (
<Container>
<Input onChange={ this.handleChange }/>
<List>
{
result.map(i => {
return (
<List.Item key={ i._id } >
<span>{ i.title }</span>
</List.Item>
)
})
}
</List>
</Container>
)
}
}
Try something like this:
export default class Example extends Component {
constructor(props) {
super(props)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.state = {
cursor: 0,
result: []
}
}
handleKeyDown(e) {
const { cursor, result } = this.state
// arrow up/down button should select next/previous list element
if (e.keyCode === 38 && cursor > 0) {
this.setState( prevState => ({
cursor: prevState.cursor - 1
}))
} else if (e.keyCode === 40 && cursor < result.length - 1) {
this.setState( prevState => ({
cursor: prevState.cursor + 1
}))
}
}
render() {
const { cursor } = this.state
return (
<Container>
<Input onKeyDown={ this.handleKeyDown }/>
<List>
{
result.map((item, i) => (
<List.Item
key={ item._id }
className={cursor === i ? 'active' : null}
>
<span>{ item.title }</span>
</List.Item>
))
}
</List>
</Container>
)
}
}
The cursor keeps track of your position in the list, so when the user presses the up or down arrow key you decrement/increment the cursor accordingly. The cursor should coincide with the array indices.
You probably want onKeyDown for watching the arrow keys instead of onChange, so you don't have a delay or mess with your standard input editing behavior.
In your render loop you just check the index against the cursor to see which one is active.
If you are filtering the result set based on the input from the field, you can just reset your cursor to zero anytime you filter the set so you can always keep the behavior consistent.
The accepted answer was very useful to me thanks! I adapted that solution and made a react hooks flavoured version, maybe it will be useful to someone:
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const useKeyPress = function(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
React.useEffect(() => {
const downHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
}, [targetKey]);
return keyPressed;
};
const items = [
{ id: 1, name: "Josh Weir" },
{ id: 2, name: "Sarah Weir" },
{ id: 3, name: "Alicia Weir" },
{ id: 4, name: "Doo Weir" },
{ id: 5, name: "Grooft Weir" }
];
const ListItem = ({ item, active, setSelected, setHovered }) => (
<div
className={`item ${active ? "active" : ""}`}
onClick={() => setSelected(item)}
onMouseEnter={() => setHovered(item)}
onMouseLeave={() => setHovered(undefined)}
>
{item.name}
</div>
);
const ListExample = () => {
const [selected, setSelected] = useState(undefined);
const downPress = useKeyPress("ArrowDown");
const upPress = useKeyPress("ArrowUp");
const enterPress = useKeyPress("Enter");
const [cursor, setCursor] = useState(0);
const [hovered, setHovered] = useState(undefined);
useEffect(() => {
if (items.length && downPress) {
setCursor(prevState =>
prevState < items.length - 1 ? prevState + 1 : prevState
);
}
}, [downPress]);
useEffect(() => {
if (items.length && upPress) {
setCursor(prevState => (prevState > 0 ? prevState - 1 : prevState));
}
}, [upPress]);
useEffect(() => {
if (items.length && enterPress) {
setSelected(items[cursor]);
}
}, [cursor, enterPress]);
useEffect(() => {
if (items.length && hovered) {
setCursor(items.indexOf(hovered));
}
}, [hovered]);
return (
<div>
<p>
<small>
Use up down keys and hit enter to select, or use the mouse
</small>
</p>
<span>Selected: {selected ? selected.name : "none"}</span>
{items.map((item, i) => (
<ListItem
key={item.id}
active={i === cursor}
item={item}
setSelected={setSelected}
setHovered={setHovered}
/>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<ListExample />, rootElement);
Attributing useKeyPress functionality to this post.
Pretty much same solution as what #joshweir provided, but in Typescript. Also instead of 'window' object I used 'ref' and added the event listeners only to the input text box.
import React, { useState, useEffect, Dispatch, SetStateAction, createRef, RefObject } from "react";
const useKeyPress = function (targetKey: string, ref: RefObject<HTMLInputElement>) {
const [keyPressed, setKeyPressed] = useState(false);
const downHandler = ({ key }: { key: string }) => {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = ({ key }: { key: string }) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
React.useEffect(() => {
ref.current?.addEventListener("keydown", downHandler);
ref.current?.addEventListener("keyup", upHandler);
return () => {
ref.current?.removeEventListener("keydown", downHandler);
ref.current?.removeEventListener("keyup", upHandler);
};
});
return keyPressed;
};
const items = [
{ id: 1, name: "Josh Weir" },
{ id: 2, name: "Sarah Weir" },
{ id: 3, name: "Alicia Weir" },
{ id: 4, name: "Doo Weir" },
{ id: 5, name: "Grooft Weir" }
];
const i = items[0]
type itemType = { id: number, name: string }
type ListItemType = {
item: itemType
, active: boolean
, setSelected: Dispatch<SetStateAction<SetStateAction<itemType | undefined>>>
, setHovered: Dispatch<SetStateAction<itemType | undefined>>
}
const ListItem = ({ item, active, setSelected, setHovered }: ListItemType) => (
<div
className={`item ${active ? "active" : ""}`}
onClick={() => setSelected(item)}
onMouseEnter={() => setHovered(item)}
onMouseLeave={() => setHovered(undefined)}
>
{item.name}
</div>
);
const ListExample = () => {
const searchBox = createRef<HTMLInputElement>()
const [selected, setSelected] = useState<React.SetStateAction<itemType | undefined>>(undefined);
const downPress = useKeyPress("ArrowDown", searchBox);
const upPress = useKeyPress("ArrowUp", searchBox);
const enterPress = useKeyPress("Enter", searchBox);
const [cursor, setCursor] = useState<number>(0);
const [hovered, setHovered] = useState<itemType | undefined>(undefined);
const [searchItem, setSearchItem] = useState<string>("")
const handelChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
setSelected(undefined)
setSearchItem(e.currentTarget.value)
}
useEffect(() => {
if (items.length && downPress) {
setCursor(prevState =>
prevState < items.length - 1 ? prevState + 1 : prevState
);
}
}, [downPress]);
useEffect(() => {
if (items.length && upPress) {
setCursor(prevState => (prevState > 0 ? prevState - 1 : prevState));
}
}, [upPress]);
useEffect(() => {
if (items.length && enterPress || items.length && hovered) {
setSelected(items[cursor]);
}
}, [cursor, enterPress]);
useEffect(() => {
if (items.length && hovered) {
setCursor(items.indexOf(hovered));
}
}, [hovered]);
return (
<div>
<p>
<small>
Use up down keys and hit enter to select, or use the mouse
</small>
</p>
<div>
<input ref={searchBox} type="text" onChange={handelChange} value={selected ? selected.name : searchItem} />
{items.map((item, i) => (
<ListItem
key={item.id}
active={i === cursor}
item={item}
setSelected={setSelected}
setHovered={setHovered}
/>
))}
</div>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<ListExample />, rootElement);
This is my attempt, with the downside that it requires the rendered children to pass ref correctly:
import React, { useRef, useState, cloneElement, Children, isValidElement } from "react";
export const ArrowKeyListManager: React.FC = ({ children }) => {
const [cursor, setCursor] = useState(0)
const items = useRef<HTMLElement[]>([])
const onKeyDown = (e) => {
let newCursor = 0
if (e.key === 'ArrowDown') {
newCursor = Math.min(cursor + 1, items.current.length - 1)
} else if (e.key === 'ArrowUp') {
newCursor = Math.max(0, cursor - 1)
}
setCursor(newCursor)
const node = items.current[newCursor]
node?.focus()
}
return (
<div onKeyDown={onKeyDown} {...props}>
{Children.map(children, (child, index) => {
if (isValidElement(child)) {
return cloneElement(child, {
ref: (n: HTMLElement) => {
items.current[index] = n
},
})
}
})}
</div>
)
}
Usage:
function App() {
return (
<ArrowKeyListManager>
<button onClick={() => alert('first')}>First</button>
<button onClick={() => alert('second')}>Second</button>
<button onClick={() => alert('third')}>third</button>
</ArrowKeyListManager>
);
}
It's a list with children that can be navigated by pressing the left-right & up-down key bindings.
Recipe.
Create an Array of Objects that will be used as a list using a map function on the data.
Create a useEffect and add an Eventlistener to listen for keydown actions in the window.
Create handleKeyDown function in order to configure the navigation behaviour by tracking the key that was pressed, use their keycodes fo that.
keyup: e.keyCode === 38
keydown: e.keyCode === 40
keyright: e.keyCode === 39
keyleft: e.keyCode === 37
Add State
let [activeMainMenu, setActiveMainMenu] = useState(-1);
let [activeSubMenu, setActiveSubMenu] = useState(-1);
Render by Mapping through the Array of objects
<ul ref={WrapperRef}>
{navigationItems.map((navigationItem, Mainindex) => {
return (
<li key={Mainindex}>
{activeMainMenu === Mainindex
? "active"
: navigationItem.navigationCategory}
<ul>
{navigationItem.navigationSubCategories &&
navigationItem.navigationSubCategories.map(
(navigationSubcategory, index) => {
return (
<li key={index}>
{activeSubMenu === index
? "active"
: navigationSubcategory.subCategory}
</li>
);
}
)}
</ul>
</li>
);
})}
</ul>
Find the above solution in the following link:
https://codesandbox.io/s/nested-list-accessible-with-keys-9pm3i1?file=/src/App.js:2811-3796

Categories

Resources