|
// Select list items in React ~ https://gist.github.com/iki/fdfd01f2bdbbed5ec55e3f8e4d7abff4 |
|
// Solution for https://jsfiddle.net/mladylukas/9qmusLok/ |
|
|
|
// Implement a feature to allow item selection with the following requirements: |
|
// 1. Clicking an item selects/unselects it. |
|
// 2. Multiple items can be selected at a time. |
|
// 3. Make sure to avoid unnecessary re-renders of each list item in the big list (performance). |
|
// 4. Currently selected items should be visually highlighted. |
|
// 5. Currently selected items' names should be shown at the top of the page. |
|
|
|
// Testing: Use React Dev Tools / Profiler / Settings / Highlight updates when components render. |
|
|
|
// TODO: Replace ReactDOM render() with ReactDOM 18 createRoot() (needs change below the line): |
|
// ReactDOM.createRoot(document.getElementById('root')).render(<List items={items} />) |
|
|
|
const { Fragment, memo, useRef, useState, useCallback } = React |
|
const { CSSTransition, TransitionGroup } = ReactTransitionGroup |
|
|
|
// Sort selected item names by color, size and fruit |
|
// TODO: Generate item index when building items (needs change below the line) |
|
const compareItems = (i1, i2) => getItemIndex(i1) - getItemIndex(i2) |
|
const getItemIndex = (item) => |
|
item.index || (item.index = generateItemIndex(item)) |
|
const generateItemIndex = (item) => { |
|
const [size, color, fruit] = item.name.split(' ', 3) |
|
return ( |
|
(colors.indexOf(color) * sizes.length + sizes.indexOf(size)) * |
|
fruits.length + |
|
fruits.indexOf(fruit) |
|
) |
|
} |
|
|
|
// Add new item or remove existing item from set and return the set |
|
const toggleItem = (set, item) => |
|
set.has(item) |
|
? console.log('-', item.name) || (set.delete(item) && set) |
|
: console.log('+', item.name) || set.add(item) |
|
|
|
const List = ({ items }) => { |
|
const [selectedCount, setSelectedCount] = useState(0) |
|
const selectedItems = useRef(new Set()).current |
|
const isSelected = (item) => selectedItems.has(item) |
|
const toggle = useCallback( |
|
(item) => setSelectedCount(toggleItem(selectedItems, item).size), |
|
[selectedItems, setSelectedCount] |
|
) |
|
|
|
return ( |
|
<Fragment> |
|
<div className="Selected"> |
|
<span className="Selected__label">Selected:</span> |
|
<SelectedItems |
|
key={!selectedCount} // Replace element when switching between none/any to avoid transition |
|
items={[...selectedItems].sort(compareItems)} |
|
/> |
|
</div> |
|
<hr /> |
|
<ul className="List"> |
|
{items.map((item) => ( |
|
<ListItem |
|
key={item.name} |
|
item={item} |
|
toggle={toggle} |
|
selected={isSelected(item)} |
|
/> |
|
))} |
|
</ul> |
|
</Fragment> |
|
) |
|
} |
|
|
|
const ListItem = memo(({ item, toggle, selected }) => ( |
|
<li |
|
className={`List__item List__item--${item.color}${ |
|
selected ? ' List__item--selected' : '' |
|
}`} |
|
onClick={() => toggle(item)} |
|
onKeyDown={() => toggle(item)} |
|
> |
|
{item.name} |
|
</li> |
|
)) |
|
|
|
const SelectedItems = ({ items }) => ( |
|
// FIXME: Transitioning items won't be memoized, needs change (and better UX) for large selections |
|
<div className="Selected__items"> |
|
<TransitionGroup component={null}> |
|
{items.map((item) => ( |
|
<SelectedItem key={item.name} item={item} /> |
|
))} |
|
</TransitionGroup> |
|
{!items.length && ( |
|
<span className="Selected__item Selected__item--none">none</span> |
|
)} |
|
<span className="Selected__item--space" /> |
|
</div> |
|
) |
|
|
|
const SelectedItem = ({ item, ...transitionProps }) => { |
|
const ref = useRef() |
|
return ( |
|
<CSSTransition |
|
nodeRef={ref} |
|
timeout={500} |
|
classNames="Selected__item-" |
|
{...transitionProps} |
|
> |
|
<span ref={ref} className={`Selected__item List__item--${item.color}`}> |
|
{item.name} |
|
</span> |
|
</CSSTransition> |
|
) |
|
} |
|
|
|
// --------------------------------------- |
|
// Do NOT change anything below this line. |
|
// --------------------------------------- |
|
|
|
const sizes = ['tiny', 'small', 'medium', 'large', 'huge'] |
|
const colors = [ |
|
'navy', |
|
'blue', |
|
'aqua', |
|
'teal', |
|
'olive', |
|
'green', |
|
'lime', |
|
'yellow', |
|
'orange', |
|
'red', |
|
'maroon', |
|
'fuchsia', |
|
'purple', |
|
'silver', |
|
'gray', |
|
'black', |
|
] |
|
const fruits = [ |
|
'apple', |
|
'banana', |
|
'watermelon', |
|
'orange', |
|
'peach', |
|
'tangerine', |
|
'pear', |
|
'kiwi', |
|
'mango', |
|
'pineapple', |
|
] |
|
|
|
const items = sizes.reduce( |
|
(items, size) => [ |
|
...items, |
|
...fruits.reduce( |
|
(acc, fruit) => [ |
|
...acc, |
|
...colors.reduce( |
|
(acc, color) => [ |
|
...acc, |
|
{ |
|
name: `${size} ${color} ${fruit}`, |
|
color, |
|
}, |
|
], |
|
[] |
|
), |
|
], |
|
[] |
|
), |
|
], |
|
[] |
|
) |
|
|
|
ReactDOM.render(<List items={items} />, document.getElementById('root')) |