Skip to content

Instantly share code, notes, and snippets.

@bsa7
Created February 20, 2019 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bsa7/7a21c78dacd57507afe64c7ea890be15 to your computer and use it in GitHub Desktop.
Save bsa7/7a21c78dacd57507afe64c7ea890be15 to your computer and use it in GitHub Desktop.
Sample of React Select
<div id="app"></div>
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"))
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