Created
February 20, 2019 14:21
-
-
Save bsa7/7a21c78dacd57507afe64c7ea890be15 to your computer and use it in GitHub Desktop.
Sample of React Select
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="app"></div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const constants = { | |
CLOSED: 'CLOSED', | |
OPENED: 'OPENED', | |
} | |
class CustomSelect extends React.Component { | |
constructor(props) { | |
super(props) | |
this.state = { | |
popupState: constants.CLOSED, | |
selectedId: undefined, // try selectedId: 1 for preselected state | |
} | |
} | |
/** | |
* Эмулятор API | |
*/ | |
getItems() { | |
return [ | |
{ | |
id: 1, | |
name: 'Отдел кадров', | |
children: [ | |
{ | |
id: 3, | |
name: 'Москва', | |
children: [ | |
{ | |
id: 6, | |
name: 'Зелёный', | |
}, | |
{ | |
id: 7, | |
name: 'Синий', | |
}, | |
{ | |
id: 8, | |
name: 'Красный', | |
children: [ | |
{ | |
id: 9, | |
name: 'Тепло, но ещё и длинно, так, чтобы в селект не влезло', | |
}, | |
{ | |
id: 10, | |
name: 'Горячо', | |
}, | |
{ | |
id: 11, | |
name: 'Холодно', | |
}, | |
] | |
}, | |
] | |
}, | |
{ | |
id: 4, | |
name: 'Екатеринбург', | |
}, | |
{ | |
id: 5, | |
name: 'Новосибирск', | |
}, | |
], | |
}, | |
{ | |
id: 2, | |
name: 'Отдел разработки', | |
}, | |
] | |
} | |
/** | |
* Генерирует повторение count символов symbol | |
* @param {Number} count - Количество повторений | |
* @param {String} symbol - символ | |
*/ | |
repeat = ({ count, symbol = '-' }) => { | |
return Array(count + 1).join(symbol) | |
} | |
/** | |
* Нерекурсивная итерация по объекту с бесконечной вложенностью | |
* @param {Function} callback - обработчик для каждого элемента массива | |
* @param {Array} items - массив объектов | |
*/ | |
traverseDeep = ({ callback, items }) => { | |
const stack = items.map((item) => { | |
return { item, level: 0 } | |
}) | |
while (stack.length) { | |
const { item, level } = stack[0] | |
const { children } = item | |
if (item.id && item.name) { | |
callback({ item, level }) | |
} | |
if (children) { | |
const nextLevelItems = item.children.map((nextLevelItem) => { | |
return { item: nextLevelItem, level: level + 1 } | |
}) || [] | |
stack.splice(1, 0, ...nextLevelItems) | |
} | |
stack.shift() | |
} | |
return undefined | |
} | |
/** | |
* Реализует нерекурсивную выборку по id из массива объектов с бесконечной вложенностью | |
* @param {Number} id - идентификатор объекта | |
* @param {Array} items - массив объектов | |
*/ | |
getDeep = ({ id, items }) => { | |
let selectedItem, selectedLevel | |
this.traverseDeep({ | |
callback: ({ item, level }) => { | |
if (item.id === id) { | |
selectedItem = item | |
selectedLevel = level | |
} | |
}, | |
items, | |
}) | |
return { selectedItem, selectedLevel } | |
} | |
/** | |
* Преобразует массив элементов меню с рекурсивно вложенной структурой в плоский массив | |
* @param {Array} items - рекурсивный массив элементов | |
*/ | |
toFlatten = ({ items }) => { | |
const itemsFlatten = [] | |
this.traverseDeep({ | |
callback: ({ item, level }) => { | |
itemsFlatten.push({ item: { id: item.id, name: item.name }, level }) | |
}, | |
items, | |
}) | |
return itemsFlatten | |
} | |
/** | |
* Открывает / закрывает выпадающее меню селекта | |
*/ | |
togglePopup = () => { | |
const { popupState } = this.state | |
const { CLOSED, OPENED } = constants | |
this.setState({ popupState: popupState === OPENED ? CLOSED : OPENED }) | |
} | |
render() { | |
const { popupState, selectedId } = this.state | |
const items = this.getItems() | |
const itemsFlatten = this.toFlatten({ items }) | |
const { selectedItem, selectedLevel } = this.getDeep({ id: selectedId, items }) || {} | |
const { name: selectedLabel } = selectedItem || {} | |
return ( | |
<div> | |
{ | |
selectedId ? ( | |
<div>Выбран id: {selectedId}</div> | |
) : ( | |
<div>Ничего не выбрано</div> | |
) | |
} | |
<div className="custom-select"> | |
<div | |
className={[ | |
"custom-select--head", | |
`custom-select--head-${popupState.toLowerCase()}` | |
].join(" ")} | |
onClick={this.togglePopup} | |
> | |
<div className="custom-select--head-text"> | |
{selectedLabel ? `${this.repeat({ count: selectedLevel + 2 })} ${selectedLabel}` : "Выбери вариант"} | |
</div> | |
</div> | |
<div | |
className={[ | |
"custom-select--popup", | |
`custom-select--popup-${popupState.toLowerCase()}`, | |
].join(" ")} | |
> | |
{ | |
itemsFlatten.map(({ item, level }, index) => { | |
return ( | |
<div | |
className={[ | |
"custom-select--popup-option", | |
item.id === selectedId ? "custom-select--popup-option-selected" : "", | |
].join(" ")} | |
key={`option_${index}`} | |
onClick={() => { | |
this.setState({ | |
popupState: constants.CLOSED, | |
selectedId: item.id, | |
}) | |
}} | |
> | |
{this.repeat({ count: level + 2 })} {item.name} | |
</div> | |
) | |
}) | |
} | |
</div> | |
</div> | |
</div> | |
) | |
} | |
} | |
ReactDOM.render(<CustomSelect />, document.querySelector("#app")) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
background: #20262E; | |
padding: 20px; | |
font-family: Helvetica; | |
} | |
#app { | |
background: #fff; | |
border-radius: 4px; | |
padding: 20px; | |
transition: all 0.2s; | |
min-height: 300px; | |
} | |
.custom-select { | |
cursor: pointer; | |
border: 2px solid lightgray; | |
display: inline-block; | |
font-size: 18px; | |
width: 300px; | |
&--head { | |
position: relative; | |
outline: 2px solid navy; | |
border-radius: 4px; | |
padding: 8px 24px 8px 8px; | |
&-text { | |
overflow: hidden; | |
white-space: nowrap; | |
} | |
&-opened, &-closed { | |
&:after { | |
position: absolute; | |
transition: all .1s; | |
background-color: white; | |
right: 8px; | |
content: '^'; | |
} | |
} | |
&-opened { | |
&:after { | |
transform: rotate(0deg); | |
top: 12px; | |
} | |
} | |
&-closed { | |
&:after { | |
transform: rotate(180deg); | |
top: 4px; | |
} | |
} | |
} | |
&--popup { | |
overflow-y: auto; | |
transition: height .1s; | |
&-closed { | |
height: 0px; | |
} | |
&-opened { | |
height: 200px; | |
} | |
&-option { | |
display: block; | |
cursor: pointer; | |
padding: 8px; | |
margin: 2px 0; | |
&:hover { | |
background-color: lightgray; | |
color: black; | |
} | |
&-selected { | |
background-color: navy; | |
color: white; | |
&:hover { | |
background-color: black !important; | |
color: white !important; | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment