Skip to content

Instantly share code, notes, and snippets.

@souporserious
Last active September 9, 2017 19:43
Show Gist options
  • Save souporserious/0e7819f3d8a567e62fc643ff821bf304 to your computer and use it in GitHub Desktop.
Save souporserious/0e7819f3d8a567e62fc643ff821bf304 to your computer and use it in GitHub Desktop.
Allows a list of items to be selected for things like highlighting or maintaining a list of selected item[s]
import React, { Component } from 'react'
const ITEM_SELECTED = 'ITEM_SELECTED'
const ITEM_DESELECTED = 'ITEM_DESELECTED'
class Controller extends Component {
static contextTypes = {
itemLists: PropTypes.object,
}
static defaultProps = {
channel: 'item-list',
}
selectedItems = []
lastSelectedItem = null
componentWillMount() {
this.itemList = this.context.itemLists[this.props.channel]
this.itemList.emitter.on(ITEM_SELECTED, this.addSelectedItem)
this.itemList.emitter.on(ITEM_DESELECTED, this.removeSelectedItem)
}
componentWillUnmount() {
this.itemList.emitter.off(ITEM_SELECTED, this.addSelectedItem)
this.itemList.emitter.off(ITEM_DESELECTED, this.removeSelectedItem)
}
addSelectedItem = item => {
this.selectedItems.push(item)
this.lastSelectedItem = item
this.forceUpdate()
}
removeSelectedItem = item => {
const position = this.selectedItems.indexOf(item)
if (position > -1) {
this.selectedItems.splice(position, 1)
}
if (item === this.lastSelectedItem) {
this.lastSelectedItem = null
}
this.forceUpdate()
}
moveSelectedItem = (amount = 0, deselectOldItem = true) => {
if (this.lastSelectedItem) {
this.itemList.updateItems((item, index) => {
if (item === this.lastSelectedItem) {
if (deselectOldItem) {
this.lastSelectedItem.deselect()
}
const newNode = this.itemList.itemNodes[index + amount]
if (newNode) {
this.itemList.items[newNode.id].select()
}
return true
}
})
} else {
const nextIndex =
amount >= 0 ? amount - 1 : this.itemList.itemNodesLength + amount
const node = this.itemList.itemNodes[nextIndex]
if (node) {
this.itemList.items[node.id].select()
}
}
}
selectAllItems = () => {
this.itemList.updateItems(item => item.select())
}
deselectAllItems = () => {
this.itemList.updateItems(item => item.deselect())
}
render() {
return this.props.children({
id: this.itemList.id,
deselectAllItems: this.deselectAllItems,
moveSelectedItem: this.moveSelectedItem,
selectAllItems: this.selectAllItems,
selectedItems: this.selectedItems,
})
}
}
import React, { Component } from 'react'
const ITEM_SELECTED = 'ITEM_SELECTED'
const ITEM_DESELECTED = 'ITEM_DESELECTED'
let itemId = 0
class Item extends Component {
static contextTypes = {
itemLists: PropTypes.object,
}
static defaultProps = {
channel: 'item-list',
}
state = {
isSelected: false,
}
componentWillMount() {
this.itemList = this.context.itemLists[this.props.channel]
this.id = `${this.itemList.id}-item-${itemId++}`
this.itemList.addItem(this)
}
componentWillUnmount() {
this.deselect()
this.itemList.removeItem(this)
}
setSelectedState = isSelected => {
if (this.props.onSelect) {
this.props.onSelect(isSelected)
}
this.setState({ isSelected })
}
getSelectedState = () => {
return this.state.isSelected
}
select = () => {
this.itemList.emitter.emit(ITEM_SELECTED, this)
this.setSelectedState(true)
}
deselect = () => {
this.itemList.emitter.emit(ITEM_DESELECTED, this)
this.setSelectedState(false)
}
toggle = () => {
this.setSelectedState(!this.state.isSelected)
}
render() {
return this.props.children({
id: this.id,
select: this.select,
deselect: this.deselect,
toggle: this.toggle,
isSelected: this.state.isSelected,
})
}
}
import React, { Component } from 'react'
import mitt from 'mitt'
let itemListId = 0
class ItemList extends Component {
static Controller = Controller
static Item = Item
static childContextTypes = {
itemLists: PropTypes.object,
}
static defaultProps = {
channel: 'item-list',
}
emitter = mitt()
id = `${this.props.channel}-${itemListId++}`
items = {}
getChildContext() {
return {
itemLists: {
...this.context.itemLists,
[this.props.channel]: this,
},
}
}
componentDidMount() {
this.setItemNodes()
}
componentDidUpdate() {
this.setItemNodes()
}
addItem = item => {
this.items[item.id] = item
}
removeItem = item => {
delete this.items[item.id]
}
setItemNodes() {
this.itemNodes = document.querySelectorAll(`[id^="${this.id}-item-"]`)
this.itemNodesLength = this.itemNodes.length
}
updateItems = cb => {
for (let index = 0; index < this.itemNodesLength; index++) {
const node = this.itemNodes[index]
const item = this.items[node.id]
if (cb(item, index)) {
return
}
}
}
render() {
return this.props.children
}
}
@souporserious
Copy link
Author

souporserious commented Sep 9, 2017

Example Usage:

const countriesList = Object.keys(countries).map(key => countries[key])

class App extends Component {
  state = {
    inputValue: 'uni',
  }
  render() {
    const { inputValue } = this.state
    return (
      <Box>
        <ItemList>
          <Box>
            <ItemList.Controller>
              {({ moveSelectedItem, selectAllItems, deselectAllItems }) =>
                <Box>
                  <Input
                    value={inputValue}
                    onChange={e =>
                      this.setState({ inputValue: e.target.value })}
                    onKeyDown={e => {
                      if (e.key === 'ArrowUp') {
                        moveSelectedItem(-1)
                      }
                      if (e.key === 'ArrowDown') {
                        moveSelectedItem(1)
                      }
                    }}
                  />
                  <Button onClick={selectAllItems} title="Select All" />
                  <Button onClick={deselectAllItems} title="Deselect All" />
                </Box>}
            </ItemList.Controller>
            {matchSorter(countriesList, inputValue, {
              keys: ['name'],
            }).map(country =>
              <ItemList.Item key={country.name} value={country}>
                {({ id, select, deselect, isSelected }) =>
                  <Box
                    id={id}
                    backgroundColor={isSelected ? 'grey-1' : 'transparent'}
                    onMouseOver={select}
                    onMouseOut={deselect}
                  >
                    {country.name}
                  </Box>}
              </ItemList.Item>
            )}
          </Box>
        </ItemList>
      </Box>
    )
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment