i am trying to create a search form using React typescript props event.I have acheived half of it but now stuck on an checkbox multiSelector where i have no idea how we can implement it.i have googled a lot but got nothing in return.
here is my code.
I am using common typescript props event onChange for setting all the values inside my search Api Object.
can anyone help me out with code or docs how we can acheive multiSelector checkbox for React Typescript props event.
1.here is my search for structure=>
enter code here
let columns = useMemo(
() => [
{
Header: "Name", accessor: "username",
Cell: (props: any) => {
if (authoritiesList.includes("USR-U")) {
let path = "/users/" + props.value;
return createClickableLink(path, props.value);
} else {
return <>
<span>{props.row.original.username}</span>
</>
}
},
FilterOptions: {
FilterInput:
<input type="text" placeholder="Username..." />,
overrideFilterLabel: "Username",
overrideFilterAccessor: "username"
}
},
{
Header: "Role(s)", accessor: "roles", disableSortBy: true,
Cell: (props: any) => {
return <>
{props.row.original.roles.map((role: any) => {
return (
<div>
<span>{role}</span><br/>
</div>)
})}
</>
},
FilterOptions: {
FilterSelect:
roleData.items.map((curRole:any)=>{
return (
<input type="checkbox value=
{curRole.name} />
)
})} ,
overrideFilterLabel: "Roles",
overrideFilterAccessor: "roles"
}
},
},
], [customerData,roleData]
)
enter code here
const selector = (state: any) => state.users;
return (
<div className="m-0 p-0 ">
<section id="content-wrapper">
<div className="row">
<div className="col-lg-12 ml-auto">
<Breadcrumb crumbs={crumbs}/>
<div className="table_data mt-2">
{createSearchButton()}
{authoritiesList.includes("USR-C") && createAddButton("/users/create", "Add User")}
<DataTable columns={columns}
fetchAction={userActions.getAllData as Dispatch<Action>}
exportAction={userActions.exportData as Dispatch<Action>}
selector={selector}/>
</div>
</div>
</div>
</section>
</div>
);
}
I want to handle multi selected checkbox event for this form in
Typescript. all forms input tags are working currently but multiselected checkbox is not working for brining output to the query object.
here is my typescript code.
for (let column of tableColumns) {
if (!column.FilterOptions) {
column.FilterOptions = {};
}
if (column.FilterOptions?.FilterSelect) {
column.FilterOptions.FilterSelect.props.onKeyPress = (event: KeyboardEvent) => {
event.key === 'Enter' && setApplyFilter(true);
}
column.FilterOptions.FilterSelect.props.onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
updateFilterQuerySelect(column, filterQuery, setFilterQuery, event);
}
}
if (column.FilterOptions?.FilterInput) {
column.FilterOptions.FilterInput.props.onKeyPress = (event: KeyboardEvent) => {
event.key === 'Enter' && setApplyFilter(true);
}
column.FilterOptions.FilterInput.props.onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFilterQuery(column, filterQuery, setFilterQuery, event);
}
}
}
here is function updateFilterQuery
const updateFilterQuery = (column: DataTableColumn, filterQuery: any, setFilterQuery: Function, event: React.ChangeEvent) => {
let tempQuery: any = {...filterQuery};
let key: string = column.FilterOptions?.overrideFilterAccessor || column.accessor;
let value: any = event.target.value;
if (event.target.value == "on" && event.target.checked != undefined) {
value = event.target.checked;
}
if (event.target.value == undefined) {
delete tempQuery[key];
} else {
key === 'phone' ? tempQuery[key] = getUnformattedPhoneNumber(value)
:
tempQuery[key] = value;
}
setFilterQuery(tempQuery);
}
It is a search form and similary it is working same as for other forms as well th eonly part missing in this form is now multiselector which is not working.
You have to separate selection state into a custom hook. A state is an array of selected items.
CodeSandbox
hooks.ts
import React, { useState } from "react";
export const useMultiselect = (initialValue: string[]) => {
const [selected, setSelected] = useState<string[]>(initialValue);
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const index = selected.indexOf(value);
if (index > -1) {
setSelected([...selected.slice(0, index), ...selected.slice(index + 1)]);
} else {
setSelected([...selected, ...[value]]);
}
};
const isSelected = (value: string) => {
return selected.includes(value);
};
return { selected, isSelected, onChange };
};
App.tsx
import { useMultiselect } from "./hooks";
const data = ["Apple", "Orange", "Banana", "Pear", "Peach"];
export default function App() {
const { selected, isSelected, onChange } = useMultiselect([]);
return (
<div>
<div>Select your favorite fruites!</div>
<ul style={{ listStyleType: "none" }}>
{data.map((value) => (
<li key={value}>
<input
id={value}
type="checkbox"
value={value}
checked={isSelected(value)}
onChange={onChange}
/>
<label htmlFor={value}>{value}</label>
</li>
))}
</ul>
<div>Selected: {selected.join()}</div>
</div>
);
}
Related
I created a form component using react hook forms. The component is composed from a group of checkboxes and a text input. The text input appears when user click on the last checkbox custom. The idea of this one is: when the user will click on it appears a text input and the user can add a custom answer/option. Ex: if user type test within the input then when the user will save the form, there should appear in an array test value, but custom text should't be in the array. In my application i don't have access to const onSubmit = (data) => console.log(data, "submit");, so i need to change the values within Component component. Now when i click on submit i get in the final array the custom value. Question: how to fix the issue described above?
const ITEMS = [
{ id: "one", value: 1 },
{ id: "two", value: 2 },
{ id: "Custom Value", value: "custom" }
];
export default function App() {
const name = "group";
const methods = useForm();
const onSubmit = (data) => console.log(data, "submit");
return (
<div className="App">
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<Component ITEMS={ITEMS} name={name} />
<input type="submit" />
</form>
</FormProvider>
</div>
);
}
export const Component = ({ name, ITEMS }) => {
const { control, getValues } = useFormContext();
const [state, setState] = useState(false);
const handleCheck = (val) => {
const { [name]: ids } = getValues();
const response = ids?.includes(val)
? ids?.filter((id) => id !== val)
: [...(ids ?? []), val];
return response;
};
return (
<Controller
name={name}
control={control}
render={({ field, formState }) => {
return (
<>
{ITEMS.map((item, index) => {
return (
<>
<label>
{item.id}
<input
type="checkbox"
name={`${name}[${index}]`}
onChange={(e) => {
field.onChange(handleCheck(e.target.value));
if (index === ITEMS.length - 1) {
setState(e.target.checked);
}
}}
value={item.value}
/>
</label>
{state && index === ITEMS.length - 1 && (
<input
{...control.register(`${name}[${index}]`)}
type="text"
/>
)}
</>
);
})}
</>
);
}}
/>
);
};
demo: https://codesandbox.io/s/winter-brook-sml0ww?file=/src/Component.js:151-1600
Assuming that the goal is to keep all the selections in the same group field, which must be an array that logs the selected values in provided order, with the custom input value as the last item if specified, perhaps ideally it would be easier to calculate the values in onSubmit before submitting.
But since the preference is not to add logic in onSubmit, maybe an alternative option could be hosting a local state, run the needed calculations when it changes, and call setValue manually to sync the calculated value to the group field.
Forked demo with modification: codesandbox
import "./styles.css";
import { Controller, useFormContext } from "react-hook-form";
import React, { useState, useEffect } from "react";
export const Component = ({ name, ITEMS }) => {
const { control, setValue } = useFormContext();
const [state, setState] = useState({});
useEffect(() => {
const { custom, ...items } = state;
const newItems = Object.entries(items).filter((item) => !!item[1]);
newItems.sort((a, b) => a[0] - b[0]);
const newValues = newItems.map((item) => item[1]);
if (custom) {
setValue(name, [...newValues, custom]);
return;
}
setValue(name, [...newValues]);
}, [name, state, setValue]);
const handleCheck = (val, idx) => {
setState((prev) =>
prev[idx] ? { ...prev, [idx]: null } : { ...prev, [idx]: val }
);
};
const handleCheckCustom = (checked) =>
setState((prev) =>
checked ? { ...prev, custom: "" } : { ...prev, custom: null }
);
const handleInputChange = (e) => {
setState((prev) => ({ ...prev, custom: e.target.value }));
};
return (
<Controller
name={name}
control={control}
render={({ field, formState }) => {
return (
<>
{ITEMS.map((item, index) => {
const isCustomField = index === ITEMS.length - 1;
return (
<React.Fragment key={index}>
<label>
{item.id}
<input
type="checkbox"
name={name}
onChange={(e) =>
isCustomField
? handleCheckCustom(e.target.checked)
: handleCheck(e.target.value, index)
}
value={item.value}
/>
</label>
{typeof state["custom"] === "string" && isCustomField && (
<input onChange={handleInputChange} type="text" />
)}
</React.Fragment>
);
})}
</>
);
}}
/>
);
};
Ok, so after a while I got the solution. I forked your sandbox and did little changes, check it out here: Save Form values in ReactJS using checkboxes
Basically, you should have an internal checkbox state and also don't register the input in the form, because this would add the input value to the end of the array no matter if that value is "".
Here is the code:
import "./styles.css";
import { Controller, useFormContext } from "react-hook-form";
import { useEffect, useState } from "react";
export const Component = ({ name, ITEMS }) => {
const { control, setValue } = useFormContext();
const [state, setState] = useState(false);
const [checkboxes, setCheckboxes] = useState(
ITEMS.filter(
(item, index) => index !== ITEMS.length - 1
).map(({ value }, index) => ({ value, checked: false }))
);
useEffect(() => {
setValue(name, []); //To initialize the array as empty
}, []);
const [inputValue, setInputValue] = useState("");
const handleChangeField = (val) => {
const newCheckboxes = checkboxes.map(({ value, checked }) =>
value == val ? { value, checked: !checked } : { value, checked }
);
setCheckboxes(newCheckboxes);
const response = newCheckboxes
.filter(({ checked }) => checked)
.map(({ value }) => value);
return state && !!inputValue ? [...response, inputValue] : response;
};
const handleChangeInput = (newInputValue) => {
const response = checkboxes
.filter(({ checked }) => checked)
.map(({ value }) => value);
if (state) if (!!newInputValue) return [...response, newInputValue];
return response;
};
return (
<Controller
name={name}
control={control}
render={({ field, formState }) => {
return (
<>
{ITEMS.map((item, index) => {
return (
<>
<label>
{item.id}
<input
type="checkbox"
name={`${name}[${index}]`}
onChange={(e) => {
if (index === ITEMS.length - 1) {
setState(e.target.checked);
return;
}
field.onChange(handleChangeField(e.target.value));
}}
value={item.value}
/>
</label>
{state && index === ITEMS.length - 1 && (
<input
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
field.onChange(handleChangeInput(e.target.value));
}}
type="text"
/>
)}
</>
);
})}
</>
);
}}
/>
);
};
I'm building a dynamic table using React-Table and i want to add a new row of editable cells.
At the moment i can add new row but only when i press the global edit button i can edit it, instead i want to add a row which would be editable at first.
This is my code -
Main component
function StyledTable() {
useEffect(() => {
dispatch(getData(mokeJsonData));
}, []);
const [datatoColumns] = useState(columnDataaa.slice(1));
const [skipPageReset, setSkipPageReset] = useState(false);
const data = useSelector((state) => state.dataReducer.data);
const dispatch = useDispatch();
const columns = useMemo(
() => [
{
Header: "",
id: "expander",
Cell2: ({ row }) => {
return (
<span {...row.getToggleRowExpandedProps()}>
{row.isExpanded ? "-" : "+"}
</span>
);
},
Cell: () => {
return <div></div>;
},
},
{
Header: columnDataaa[0].Header,
accessor: columnDataaa[0].accessor,
Cell: ({ value, row }) => {
return (
<FlexDiv>
<HighlightOffIcon
style={{ marginRight: "5px", color: "grey", width: "20px" }}
onClick={() => dispatch(deleteRow(row.index))}
/>
{value}
</FlexDiv>
);
},
},
...datatoColumns,
],
[]
);
useEffect(() => {
setSkipPageReset(false);
}, [data]);
const renderRowSubComponent = useCallback(
({ row }) => ({
values: row.original.addInfo && row.original.addInfo,
}),
[]
);
return (
<Styles>
<h1>הגדרת מנהל</h1>
<Table
columns={columns}
skipPageReset={skipPageReset}
renderRowSubComponent={renderRowSubComponent}
/>
</Styles>
);
}
export default StyledTable;
Editable Cell
const EditableCell = ({
value: initialValue,
row: { index },
column: { id, editable, type, width, valueOptions },
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const dispatch = useDispatch();
const onBlur = () => {
if (value === "") return alert("requiredddd");
return dispatch(updateMyData({ index, id, value }));
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
if (type === "singleSelect")
return (
<InputSelect
value={value}
onChange={onChange}
onBlur={onBlur}
style={{ width: width }}
>
{valueOptions.map((item, i) => {
return (
<option value={item.label} key={i}>
{item.label}
</option>
);
})}
</InputSelect>
);
if (type === "date")
return (
<DatePicker
style={{ width: width }}
type="date"
disabled={editable === false}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
return (
<input
style={{ width: width }}
disabled={editable === false}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
export default EditableCell;
Add Row function
addRow: (state, action) => {
const obj = {};
action.payload.slice(1).forEach((item) => {
obj[item.accessor] = '';
});
if (
obj &&
Object.keys(obj).length === 0 &&
Object.getPrototypeOf(obj) === Object.prototype
)
return;
else {
state.data.splice(0, 0, obj);
state.originalData = state.data;
}
},
Thanks
Pass the state variable and method to the useTable() root hook. custom plugin hooks and other variables/methods maintaining the component state are returned from the table instance. These you can later retrieve from anywhere you want.
const {
// all your hooks...
} = useTable(
{
columns,
data,
// all your other hooks...
updateMyData,
// pass state variables so that we can access them in edit hook later
editableRowIndex, // index of the single row we want to edit
setEditableRowIndex // setState hook for toggling edit on/off switch
},
// other hooks...
(hooks) => {
hooks.allColumns.push((columns) => [
// other hooks such as selection hook
...columns,
// edit hook
{
accessor: "edit",
id: "edit",
Header: "edit",
Cell: ({ row, setEditableRowIndex, editableRowIndex }) => (
<button
className="action-button"
onClick={() => {
const currentIndex = row.index;
if (editableRowIndex !== currentIndex) {
// row requested for edit access
setEditableRowIndex(currentIndex);
} else {
// request for saving the updated row
setEditableRowIndex(null); // keep the row closed for edit after we finish updating it
const updatedRow = row.values;
console.log("updated row values:");
console.log(updatedRow);
// call your updateRow API
}
}}
>
{/* single action button supporting 2 modes */}
{editableRowIndex !== row.index ? "Edit" : "Save"}
</button>
)
}
]);
}
);
you can found example from bellow link
github repo link: https://github.com/smmziaul/only-one-row-editable
code sandbox link: https://codesandbox.io/s/github/smmziaul/only-one-row-editable
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
I want to filter over an array using react hooks. It should be a fairly straight forward task, but I am assuming it is something to do with hooks being updated asynchronously, although this could be wrong.
I am fairly stumped, but have included a code sandbox and my code below:
const teams_data = [
"tottenham",
"arsenal",
"man utd",
"liverpool",
"chelsea",
"west ham"
];
function App() {
const [teams, setTeams] = React.useState(teams_data);
const [search, setSearch] = React.useState("");
return (
<div className="App">
<input
onChange={e => {
const test = teams.filter(team => {
return team.toLowerCase().includes(e.target.value.toLowerCase());
});
console.log("test: ", test);
// uncomment line below and teams is logged as I want
setTeams(test);
setSearch(e.target.value);
}}
type="text"
value={search}
/>
{teams.map(team => (
<p>{team}</p>
))}
</div>
);
}
You need to filter the original data :
const test = teams_data.filter(team => {
return team.toLowerCase().includes(e.target.value.toLowerCase());
});
https://codesandbox.io/s/thirsty-austin-uqx8k
You just need to add another state for search results
const [data , setData] = useState(teams);
const [query, setQuery] = useState('')
const[res , setRes] = useState([]);
return (
<div className="App container">
<form onSubmit = {(e) => e.preventDefault()}>
<input type = "search" className = "srh" placeholder = "search about..."
onChange = {(e) => {
const test = data.filter(team => {
return (
team.toLowerCase().includes(e.target.value.toLowerCase())
)
})
setRes(test)
if(e.target.value === '') setRes([])
}}
/>
</form>
<div>
{
res.map((item , i) => (
<p key = {i}>{item}</p>
))
}
</div>
</div>
);
I've made custom hook.
It receives the array as a first param
the search variable as a second
and the property you want to filter by
I hope it's helpfull
export function useSearch(array: any[], search: string, field: string) {
const filteredArray = array.filter((entry) => {
if (search === "") return entry;
else if (
entry[field].toLocaleLowerCase().includes(search.toLocaleLowerCase())
)
return entry;
});
return {
filteredArray
};
}
Them apply the filtered array to your map function
import { useSearch } from "./useSearch";
import { useState } from "react";
const array = [
{
id: 1,
name: "Humberto Guenzo Yoshimoto"
},
{
id: 2,
name: "Diego Braga"
},
{
id: 3,
name: "Hudson Teixeira"
},
{
id: 4,
name: "Matheus Doimo"
}
];
type FilteredArrayTypes = {
id: number;
name: string;
};
export default function App() {
const [searchByFullName, setSearchByFullName] = useState("");
const { filteredArray } = useSearch(array, searchByFullName, "name");
return (
<div className="App">
<h1>Search list</h1>
<input
onChange={(e) => setSearchByFullName(e.target.value)}
type="text"
value={searchByFullName}
placeholder="search"
/>
{filteredArray.map((entry: FilteredArrayTypes) => {
return (
<ul>
<li>{entry.name}</li>
</ul>
);
})}
</div>
);
}
Here goes a sandbox with the code: here
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