I am trying to use buttons to filter data from JSON. Whenever a button is clicked, it will update the state with filtered JSON data. It will then return the list with the updated data. Right now I have four buttons to filter the four different "types" but the code is not working at all.
import React, { useState, useEffect } from 'react';
import * as moment from 'moment';
import PollCard from './PollCard'
function PollList() {
const [polls, setPolls] = useState(null);
const [loading, setLoading] = useState(false);
const range = 30
var dateRange = moment().subtract(range, 'days').calendar();
async function fetchMyAPI() {
let response = await fetch(url)
const json = await response.json()
var data = json.filter(e => Date.parse(e.endDate) >= Date.parse(dateRange))
setPolls(data.reverse())
setLoading(true);
}
useEffect(() => {
fetchMyAPI();
}, [])
if (!loading) {
return ("Loading...")
}
var A = polls.filter(e => e.type === "A")
var B = polls.filter(e => e.type === "B")
var C = polls.filter(e => e.type === "C")
function showA() {
setPolls(A)
}
function showB() {
setPolls(B)
}
function showC() {
setPolls(C)
}
return (
<div className="polls">
<button onClick={showA()}>A</button>
<button onClick={showB()}>B</button>
<button onClick={showC()}>C</button>
{
polls && polls.map((poll) => (
<div key={poll.id}>
<PollCard poll={poll} />
<hr style={{ opacity: '.1' }} />
</div>
))
}
</div>
);
}
export default PollList;
You need two arrays to properly filter your data, when you click one of the buttons, it overwrites the original data. Change <button onClick={showA()}>A</button> to <button onClick={() => showA()}>A</button>
// get data from api, won't change
const [polls, setPolls] = useState(null);
// used for displaying polls and filtering
const [filteredPolls, setfilteredPolls] = useState(null)
Filtering data
var A = polls.filter(e => e.type === "A")
var B = polls.filter(e => e.type === "B")
var C = polls.filter(e => e.type === "C")
function showA() {
setfilteredPolls(A)
}
function showB() {
setfilteredPolls(B)
}
function showC() {
setfilteredPolls(C)
}
Displaying data
return (
<div className="polls">
<button onClick={() => showA()}>A</button>
<button onClick={() => showB()}>B</button>
<button onClick={() => showC()}>C</button>
{
filteredPolls && filteredPolls.map((poll) => (
<div key={poll.id}>
<PollCard poll={poll} />
<hr style={{ opacity: '.1' }} />
</div>
))
}
</div>
);
DEMO
<script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script crossorigin src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
class PollCard extends React.Component {
render() {
return (
<div>{`${this.props.poll.name} - ${this.props.poll.type}`}</div>
)
}
}
const dpolls = [
{
id: 4,
type: 'C',
name: 'Test 1'
},
{
id: 6,
type: 'B',
name: 'Test 2'
},
{
id: 7,
type: 'A',
name: 'Test 3'
},
{
id: 8,
type: 'A',
name: 'Test 9'
},
{
id: 17,
type: 'B',
name: 'Test 39'
}
]
function PollList() {
const [polls, setPolls] = React.useState(null);
const [filteredPolls, setfilteredPolls] = React.useState(null)
const [loading, setLoading] = React.useState(false);
const range = 30
// var dateRange = moment().subtract(range, 'days').calendar();
async function fetchMyAPI() {
let response = await fetch('https://api.themoviedb.org/3/movie/upcoming?api_key=81f382d33088c6d52099a62eab51d967&language=en-US&page=1')
const json = await response.json()
// var data = json.filter(e => Date.parse(e.endDate) >= Date.parse(dateRange))
setPolls(dpolls);
setfilteredPolls(dpolls.filter(e => e.type === "A"));
setLoading(true);
}
React.useEffect(() => {
fetchMyAPI();
}, [])
if (!loading) {
return ("Loading...")
}
var A = polls.filter(e => e.type === "A")
var B = polls.filter(e => e.type === "B")
var C = polls.filter(e => e.type === "C")
function showA() {
setfilteredPolls(A)
}
function showB() {
setfilteredPolls(B)
}
function showC() {
setfilteredPolls(C)
}
function removeFiter() {
setfilteredPolls(polls);
}
return (
<div className="polls">
<button onClick={() => showA()}>A</button>
<button onClick={() => showB()}>B</button>
<button onClick={() => showC()}>C</button>
<button onClick={() => removeFiter()}>Remove Filter</button>
{
filteredPolls && filteredPolls.map((poll) => (
<div key={poll.id}>
<PollCard poll={poll} />
<hr style={{ opacity: '.1' }} />
</div>
))
}
</div>
);
}
class App extends React.Component {
constructor() {
super();
this.state = {
name: 'React'
};
}
render() {
return (
<div>
<PollList />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
Related
import React from 'react' import { useState } from 'react'
const App = () => {
const [tasks, setTasks] = useState([
{ id: 1, text: 'Task1' }, { id: 2, text: 'Task2' }, { id: 3, text: 'Task3' }
])
const showTasks = tasks.map((task) => <h2>{task.text}<button onClick={() => onDelete(task.id)}>X</button></h2>)
const onDelete = (id) => {
setTasks(tasks.filter((task) => task.id !== id))
}
const [text, setText] = useState('')
const onSubmit = (e) => {
e.preventDefault()
if (!text || tasks > text || text === setTasks) {
alert('Problem');
return
}
addTask({ text })
setText('')
}
const addTask = (task) => {
const id = Math.floor(Math.random() * 10000) + 1;
const newTask = { id, ...task };
setTasks([...tasks, newTask]);
}
return (
<div>
<div>{showTasks}</div>
<form onSubmit={onSubmit}>
<input type='text' value={text} onChange={(e) => setText(e.target.value)} />
<input type='submit' value='Save Task' />
</form>
</div>
) }
export default App
Following is the condition to avoid duplicates in the onSubmit function.
!text || tasks.some((item) => item.text === text)
const App = () => {
const [tasks, setTasks] = React.useState([
{ id: 1, text: "Task1" },
{ id: 2, text: "Task2" },
{ id: 3, text: "Task3" }
]);
const showTasks = tasks.map((task) => (
<h2 key={task.id}>
{task.text}
<button onClick={() => onDelete(task.id)}>X</button>
</h2>
));
const onDelete = (id) => {
setTasks(tasks.filter((task) => task.id !== id));
};
const [text, setText] = React.useState("");
const onSubmit = (e) => {
e.preventDefault();
if (!text || tasks.some((item) => item.text === text)) {
alert("Duplicate");
return;
}
addTask({ text });
setText("");
};
const addTask = (task) => {
const id = Math.floor(Math.random() * 10000) + 1;
const newTask = { id, ...task };
setTasks([...tasks, newTask]);
};
return (
<div>
<div>{showTasks}</div>
<form onSubmit={onSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<input type="submit" value="Save Task" />
</form>
</div>
);
};
ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='react'></div>
I'm working on a React Notes Application and my App.js contains all the necessary functions props which are passed down to several components.
As a result I'm doing prop drilling a lot where I'm passing down around 10-20 props/functions in the components where it isn't needed.
I tried using useContext Hook but I guess it doesn't work with callback functions in the value parameter.
App.js
const App = () => {
const [ notes, setNotes ] = useState([]);
const [ category, setCategory ] = useState(['Notes']);
const [ searchText, setSearchText ] = useState('');
const [ alert, setAlert ] = useState({
show:false,
msg:'',
type:''
});
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState(null);
useEffect(()=>{
keepTheme();
})
// retreive saved notes
useEffect(()=>{
const savedNotes = JSON.parse(localStorage.getItem('react-notes-data'));
if (savedNotes) {
setNotes(savedNotes)
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('react-notes-data', JSON.stringify(notes))
setNotesCopy([...notes]);
}, [notes]);
// save button will add new note
const addNote = (text) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
category: category,
}
const newNotes = [...notes, newNote]
setNotes(newNotes);
}
const deleteNote = (id) => {
showAlert(true, 'Note deleted', 'warning');
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
}
// hardcoded values for categories
const allCategories = ['Notes', 'Misc', 'Todo', 'Lecture Notes', 'Recipe'];
// copy notes for filtering through
const [notesCopy, setNotesCopy] = useState([...notes]);
const handleSidebar = (category) => {
setNotesCopy(category==='Notes'?[...notes]:
notes.filter(note=>note.category===category));
}
// function to call alert
const showAlert = (show=false, msg='', type='') => {
setAlert({show, msg, type});
}
return (
<div>
<div className="container">
<Sidebar
allCategories={allCategories}
handleSidebar={handleSidebar}
notesCopy={notesCopy}
key={notes.id}
/>
<Header notes={notes} alert={alert} removeAlert={showAlert} />
<Search handleSearchNote={setSearchText} />
<NotesList
notesCopy={notesCopy.filter(note=>
note.text.toLowerCase().includes(searchText) ||
note.category.toString().toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
deleteNote={deleteNote}
category={category}
setCategory={setCategory}
allCategories={allCategories}
showAlert={showAlert}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
</div>
</div>
)
}
NotesList.js
const NotesList = (
{ notesCopy, handleAddNote, deleteNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, notes, setNotes, editId, setEditId }
) => {
const [ noteText, setNoteText ] = useState('');
const textareaRef = useRef();
// function to set edit notes
const editItem = (id) => {
const specificItem = notes.find(note=>note.id === id);
setNoteText(specificItem.text);
setIsEditing(true);
setEditId(id);
textareaRef.current.focus();
}
return (
<div key={allCategories} className="notes-list">
{notesCopy.map(note => {
return (
<Note
key={note.id}
{...note}
deleteNote={deleteNote}
category={note.category}
isEditing={isEditing}
editId={editId}
editItem={editItem}
/>)
})}
<AddNote
handleAddNote={handleAddNote}
category={category}
setCategory={setCategory}
showHideClassName={showHideClassName}
allCategories={allCategories}
showAlert={showAlert}
isEditing={isEditing}
setIsEditing={setIsEditing}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
noteText={noteText}
setNoteText={setNoteText}
textareaRef={textareaRef}
/>
</div>
)
}
AddNote.js
const AddNote = ({ notes, setNotes, handleAddNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, editId, setEditId, noteText, setNoteText, textareaRef }) => {
const [ show, setShow ] = useState(false);
const [ modalText, setModalText ] = useState('');
const charCount = 200;
const handleChange = (event) => {
if (charCount - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
}
const handleSaveClick = () => {
if (noteText.trim().length === 0) {
setModalText('Text cannot be blank!');
setShow(true);
}
if (category === '') {
setModalText('Please select a label');
setShow(true);
}
if (noteText.trim().length > 0 && category!=='') {
showAlert(true, 'Note added', 'success');
handleAddNote(noteText);
setNoteText('');
setShow(false);
}
if (noteText.trim().length > 0 && category!=='' && isEditing) {
setNotes(notes.map(note=>{
if (note.id === editId) {
return ({...note, text:noteText, category:category})
}
return note
}));
setEditId(null);
setIsEditing(false);
showAlert(true, 'Note Changed', 'success');
}
}
const handleCategory = ( event ) => {
let { value } = event.target;
setCategory(value);
}
showHideClassName = show ? "modal display-block" : "modal display-none";
return (
<div className="note new">
<textarea
cols="10"
rows="8"
className='placeholder-dark'
placeholder="Type to add a note.."
onChange={handleChange}
value={noteText}
autoFocus
ref={textareaRef}
>
</textarea>
<div className="note-footer">
<small
className='remaining'
style={{color:(charCount - noteText.length == 0) && '#c60000'}}>
{charCount - noteText.length} remaining</small>
<div className='select'>
<select
name={category}
className="select"
onChange={(e)=>handleCategory(e)}
required
title='Select a label for your note'
defaultValue="Notes"
>
<option value="Notes" disabled selected>Categories</option>
{allCategories.map(item => {
return <option key={item} value={item}>{item}</option>
})}
</select>
</div>
<button className='save' onClick={handleSaveClick} title='Save note'>
<h4>{isEditing ? 'Edit':'Save'}</h4>
</button>
</div>
{/* Modal */}
<main>
<div className={showHideClassName}>
<section className="modal-main">
<p className='modal-text'>{modalText}</p>
<button type="button" className='modal-close-btn'
onClick={()=>setShow(false)}><p>Close</p>
</button>
</section>
</div>
</main>
</div>
)
}
I want the functions passed from App.js to NotesList.js to be in AddNote.js without them being passed in NotesList.js basically minimizing the props destructuring in NotesList.js
Context API does work with function. What you need to do is pass your function to Provider inside value :
<MyContext.Provider value={{notes: notesData, handler: myFunction}} >
For example:
// notes-context.js
import React, { useContext, createContext } from 'react';
const Context = createContext({});
export const NotesProvider = ({children}) => {
const [notes, setNote] = useState([]);
const addNote = setNote(...); // your logic
const removeNote = setNote(...); // your logic
return (
<Context.Provider value={{notes, addNote, removeNote}}>
{children}
</Context.Provider>
)
}
export const useNotes = () => useContext(Context);
Add Provider to your App.js like so:
// App.js
import NoteProvider from './notes-context';
export default App = () => {
return (
<NoteProvider>
<div>... Your App</div>
</NoteProvider>
)
}
Then call UseNote in your NoteList.js to use the function:
// NoteList.js
import {useNotes} from './note-context.js';
export const NoteList = () => {
const {notes, addNotes, removeNotes} = useNotes();
// do your stuff. You can now use functions addNotes and removeNotes without passing them down the props
}
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
Guys I need help to make an API search request that should be done when page is ready.
This is state object:
const [state, setState] = useState({
s: "",
results: [],
selected: {}
});
const apiurl = ....;
This is how my search input actually works:
const search = (e) => {
if (e.key === "Enter") {
axios(apiurl + "&s=" + state.s).then(({data}) => {
let results = data.Search;
setState(prevState => {
return {...prevState, results: results }
});
});
}
}
const handleInput = (e) => {
let s = e.target.value;
setState(prevState => {
return { ...prevState, s: s }
});
}
My components:
return (
<div className='basic'>
<Header />
<Search handleInput={handleInput} search={search} />
<Container>
<Results results={state.results} openPopup={openPopup} />
{(typeof state.selected.Title != "undefined") ? <Popup selected={state.selected} closePopup={closePopup} /> : false }
</Container>
</div>
);
Search.js:
function Search ({ handleInput, search})
return (
<Container bg="dark" variant="dark">
<section className="searchbox-wrap">
<input
type="text"
placeholder="Поиск фильма"
className="searchbox"
onChange={handleInput}
onKeyPress={search}
/>
</section>
</Container>
)
Results.js:
function Results ({ results, openPopup }) {
return (
<section className="results">
{results.map(result => (
<Result key={result.imdbID} result={result} openPopup={openPopup} />
))}
</section>
);
}
So how can I make search request (for example: Superman) be done when page is loaded? Thank you!
You can do that with useEffect hook, that is equivalent to componentDidMount() lifecycle method when an empty array is passed as a second argument. So modified code would look like the following:
import React, { useState, useEffect, useCallback } from 'react';
function SearchComponent() {
const [state, setState] = useState({
s: "Superman",
results: [],
selected: {}
});
const apiurl = "";
const makeSearchRequest = useCallback(
searchString => {
axios(apiurl + "&s=" + searchString)
.then(({ data }) => data.Search)
.then(results => setState(prevState => ({ ...prevState, results })));
},
[setState]
);
// This will be invoked only on component mount
useEffect(() => makeSearchRequest(state.s), []);
const handleInput = useCallback(
e => {
e.persist();
setState(prevState => ({ ...prevState, s: e.target.value }));
},
[setState]
);
const search = useCallback(
e => {
if (e.key === "Enter") {
makeSearchRequest(state.s);
}
},
[makeSearchRequest, state.s]
);
return (
<input
type="text"
value={state.s}
onChange={handleInput}
onKeyPress={search}
/>
);
}
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