@render-props/choices
Version:
A state container which provides an interface for making selections from a group of choices. The `Choices` component itself is a context provider which can be used with the `Choice` and `ChoicesConsumer` components for deep-tree selections. It does not ha
258 lines (226 loc) • 6.88 kB
JavaScript
import React from 'react'
import PropTypes from 'prop-types'
import {callIfExists} from '@render-props/utils'
import Items from '@render-props/items'
import ChoicesContext from './ChoicesContext'
import {includesInvariant} from './invariants'
import {boundChoices, boundSelections} from './utils'
/**
import Choices, {Choice} from '@render-props/choices'
const FavoritePets = props => (
<Choices
initialChoices={new Set(['cat', 'dog', 'turtle'])}
initialSelections={new Set(['cat'])}
minChoices={1}
minSelections={1}
maxSelections={2}
onBoundMaxSelections={
function ({selections, select, deselect}) {
selections = Array.from(selections)
deselect(selections.shift())
select(selections.pop())
}
}
>
{PetsControl}
</Choices>
)
const PetsControl = ({
addChoice,
deleteChoice,
setChoices,
clearChoices,
isChoice,
select,
deselect,
setSelections,
clearSelections,
isSelected,
selections,
choices
}) => (
<div style={{borderWidth: 1}}>
<span>
Number of favorites: {selections.size}
</span>
{
Array.from(choices).map(pet => (
<Choice key={pet} value={pet}>
{
function ({select, deselect, toggle, isSelected}) {
return (
<button
onClick={toggle}
style={
isSelected
? {backgroundColor: 'green'}
: {backgroundColor: 'grey'}
}
>
{pet}
</button>
)
}
}
</Choice>
))
}
</div>
)
*/
export default function Choices (props) {
const nextProps = {children: props.children}
return (
<Items
initialItems={props.initialChoices}
minItems={props.minChoices}
maxItems={props.maxChoices}
onAdd={props.onAddChoice}
onDelete={props.onDeleteChoice}
onChange={props.onChoicesChange}
onBoundMax={boundChoices(props.onBoundMinChoices)}
onBoundMin={boundChoices(props.onBoundMaxChoices)}
>
{function (choicesContext) {
nextProps.addChoice = choicesContext.addItem
nextProps.deleteChoice = choicesContext.deleteItem
nextProps.clearChoices = choicesContext.clearItems
nextProps.setChoices = choicesContext.setItems
nextProps.isChoice = choicesContext.includes
nextProps.choices = choicesContext.items
return (
<Items
initialItems={props.initialSelections}
minItems={props.minSelections}
maxItems={props.maxSelections}
onAdd={props.onSelect}
onDelete={props.onDeselect}
onChange={props.onChange}
onBoundMin={boundSelections(props.onBoundMinSelections)}
onBoundMax={boundSelections(props.onBoundMaxSelections)}
>
{function (selectionsContext) {
nextProps.select = selectionsContext.addItem
nextProps.deselect = selectionsContext.deleteItem
nextProps.setSelections = selectionsContext.setItems
nextProps.isSelected = selectionsContext.includes
nextProps.clearSelections = selectionsContext.clearItems
nextProps.selections = selectionsContext.items
return <Choices_ {...nextProps}/>
}}
</Items>
)
}}
</Items>
)
}
Choices.propTypes = {
children: PropTypes.func.isRequired,
// choices
initialChoices: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Set)]),
minChoices: PropTypes.number,
maxChoices: PropTypes.number,
onAddChoice: PropTypes.func,
onDeleteChoice: PropTypes.func,
onChoicesChange: PropTypes.func,
onBoundMinChoices: PropTypes.func,
onBoundMaxChoices: PropTypes.func,
// selections
initialSelections: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Set)]),
minSelections: PropTypes.number,
maxSelections: PropTypes.number,
onSelect: PropTypes.func,
onDeselect: PropTypes.func,
onChange: PropTypes.func,
onBoundMinSelections: PropTypes.func,
onBoundMaxSelections: PropTypes.func,
}
class Choices_ extends React.Component {
static displayName = 'Choices'
constructor (props) {
super(props)
this.choicesContext = {
select: this.select,
deselect: this.deselect,
toggle: this.toggle,
setSelections: this.setSelections,
clearSelections: this.props.clearSelections,
isSelected: this.props.isSelected,
addChoice: this.props.addChoice,
deleteChoice: this.deleteChoice,
setChoices: this.setChoices,
clearChoices: this.clearChoices,
isChoice: this.props.isChoice,
selections: null,
choices: null
}
}
ifChoicesInclude (items) {
if (typeof process !== void 0 && process.env.NODE_ENV !== 'production') {
for (let x = 0; x < items.length; x++) {
includesInvariant(this.props.choices, items[x])
}
}
}
select = (...items) => {
this.ifChoicesInclude(items)
return this.props.select(...items)
}
deselect = (...items) => {
this.ifChoicesInclude(items)
return this.props.deselect(...items)
}
toggle = item => this.props.isSelected(item) ?
this.deselect(item) :
this.select(item)
setSelections = items => {
this.ifChoicesInclude(items)
return this.props.setSelections(items)
}
deleteChoice = (...items) => {
const {
isSelected,
minSelections,
deleteChoice
} = this.props
const whichSize = this.props.selections.size === void 0 ? 'length' : 'size'
// removes all selections referencing this choice
for (let x = 0; x < items.length; x++) {
const item = items[x]
if (isSelected(item)) {
this.deselect(item)
if (minSelections > (this.props.selections[whichSize] - 1)) {
return
}
}
}
// deletes the choice
return deleteChoice(...items)
}
setChoices = items => {
const diff = Array.from(items).filter(item => this.props.isChoice(item))
if (diff.length) {
this.props.deleteChoice(...diff)
}
return this.props.setChoices(items)
}
clearChoices = () => {
this.props.clearSelections()
this.props.clearChoices()
}
render () {
if (
this.choicesContext.selections !== this.props.selections
|| this.choicesContext.choices !== this.props.choices
) {
this.choicesContext = {...this.choicesContext}
}
this.choicesContext.selections = this.props.selections
this.choicesContext.choices = this.props.choices
return (
<ChoicesContext.Provider value={this.choicesContext}>
{this.props.children(this.choicesContext)}
</ChoicesContext.Provider>
)
}
}