React level linkage, city cascade selection, multi-level linkage components

Implement custom multi-level hierarchical components based on antd, mainly using react + antd + typescript to achieve specific content:

Component display effect:
data json format:

[{<!-- -->
text: 'Zhejiang Province',
value: '330000',
children: [{<!-- -->
text: 'Hangzhou City',
value: '330100',
children: [{<!-- -->
text: 'Uptown',
value: '330102',
}, {<!-- -->
text: 'Downtown',
value: '330103',
}, {<!-- -->
text: 'Jianggan District',
value: '330104',
}]
}, {<!-- -->
text: 'Ningbo City',
value: '330200',
children: [{<!-- -->
text: 'Haishu District',
value: '330203',
}, {<!-- -->
text: 'Jiangbei District',
value: '330205',
}, {<!-- -->
text: 'Beilun District',
value: '330206',
}]
}, {<!-- -->
text: 'Wenzhou',
value: '330300',
children: [{<!-- -->
text: 'Lucheng District',
value: '330302',
}, {<!-- -->
text: 'Longwan District',
value: '330303',
}, {<!-- -->
text: 'Ouhai District',
value: '330304',
}]
}]
}, {<!-- -->
text: 'Jiangsu Province',
value: '320000',
children: [{<!-- -->
text: 'Nanjing',
value: '320100',
children: [{<!-- -->
text: 'Xuanwu District',
value: '320102',
}, {<!-- -->
text: 'Qinhuai District',
value: '320104',
}, {<!-- -->
text: 'Jianye District',
value: '320105',
}]
}, {<!-- -->
text: 'Wuxi City',
value: '320200',
children: [{<!-- -->
text: 'Xishan District',
value: '320205',
}, {<!-- -->
text: 'Huishan District',
value: '320206',
}, {<!-- -->
text: 'Binhu District',
value: '320211',
}]
}, {<!-- -->
text: 'Xuzhou City',
value: '320300',
children: [{<!-- -->
text: 'Gulou District',
value: '320302',
}, {<!-- -->
text: 'Yunlong District',
value: '320303',
}, {<!-- -->
text: 'Jiawang District',
value: '320305',
}]
}]
}]

CascaderList.tsx component source code:

import {<!-- --> useState, memo, useMemo, useEffect } from 'react';
import {<!-- --> Popover, Cascader } from 'antd';
import {<!-- --> CheckOutlined, CaretDownOutlined, CloseCircleFilled } from '@ant-design/icons';
import '@/assets/style/cascaderList.less';

type objTypes = {<!-- -->
[key: string]: any
}

type fileNameTypes = {<!-- -->
text: string,
value: string,
children: string
}

type propTypes = {<!-- -->
column?: number, //several levels of linkage
fileNames?: fileNameTypes, //Attributes in the option list {text: 'text', value: 'value', children: 'children'}
options: Array<objTypes>, // list of configuration options
checkedValue?: Array<any>, //checked data
placeholder?: string, // input placeholder
separator?: string, // separator
onFinish?: Function, //Callback function for all selections
render?: Function, //custom rendering content
children?: JSX.Element
}

// Convert the tree structure to an array
function treeToList(tree: Array<objTypes>) {<!-- -->
let queue = [...tree];
let res: Array<objTypes> = [];
queue.concat(tree);
while (queue. length != 0) {<!-- -->
let obj = queue.shift();// Pop up the first element of the queue
if (obj?.children) {<!-- -->
queue = queue.concat(obj.children);// child nodes enqueue
// delete obj["children"];// delete the children property
}
res.push(obj as objTypes);
}
return res;
};

// configure the displayed navigation
function configNav<T>(checkedList: Array<T>, col: number): Array<T> {<!-- -->
let arr: Array<T> = [];
if (checkedList. length >= col) {<!-- -->
arr = checkedList;
} else {<!-- -->
arr = [...checkedList, 'Please choose' as T];
}
return arr;
};

// convert the field to display
function renderTextCallback<T>(checkedValue: Array<T>, allList: Array<objTypes>, fileNames: fileNameTypes, keyText: string): Array<T> {<!-- -->
if (!checkedValue. length) return [];
let key = keyText === 'text' ? fileNames.value : fileNames.text;
return checkedValue.map(item => {<!-- -->
let findItem = allList. find((el: objTypes) => el[key] === item);
return (findItem ? findItem[keyText] : '');
})
};

/* Rendering layer linkage list */
function CascaderList(props: propTypes) {<!-- -->

const {<!-- --> column = 3, fileNames = {<!-- -->
text: 'text',
value: 'value',
children: 'children'
},
checkedValue = [], options = [], separator = '/', placeholder, onFinish, render } = props;

const allCascaderList = useMemo(() => {<!-- -->
return treeToList(options);
}, [])

// control popup display
const [open, setOpen] = useState(false);
// currently selected data
const [checkedList, setCheckedList] = useState<string[]>(() => renderTextCallback<string>(checkedValue, allCascaderList, fileNames, 'text'));
// current rendering selected
const [renderCheckedText, setRenderCheckedText] = useState(checkedList);
// currently selected navigation
const [activeKey, setActiveKey] = useState(checkedValue. length ? checkedValue. length - 1 : 0);
// configure navigation
const [tabList, setTabList] = useState(() => configNav(checkedList, column));

useEffect(() => {<!-- -->
if (!checkedValue. length) {<!-- -->
setCheckedList([]);
setRenderCheckedText([]);
setActiveKey(0);
setTabList(configNav([], column));
}
}, [JSON. stringify(checkedValue)])


/* Modify navigation changes */
const onChange = (key: number) => {<!-- -->
if (key === activeKey) return;
setActiveKey(key);
let newTabList = tabList.filter(item => item !== 'Please select');
if (JSON.stringify(newTabList) !== JSON.stringify(tabList)) {<!-- -->
setTabList(newTabList);
};
};
/* Click to select the level */
const handleSelectCascader = (e: any, label: string) => {<!-- -->
e. stopPropagation();
// set selected data
let newCheckedList = checkedList. slice(0, activeKey);
newCheckedList. push(label);
setCheckedList(newCheckedList);
if (newCheckedList. length === column) {<!-- -->
// If the selected data is the same as the number of hierarchical linkages, the selection is completed
onSelectCascaderFinish(newCheckedList)
} else {<!-- -->
// set navigation
let newTabList = configNav(newCheckedList, column);
setTabList(newTabList);
setActiveKey(activeKey + 1);
}
};
/* Configure the list of renders */
const renderCallback = (checkedList: string[], tabKey: number): objTypes => {<!-- -->
if (!checkedList.length || tabKey === 0) return options;
return allCascaderList.find(item => item.text === checkedList[tabKey - 1])?.children;
};
/* Currently selected information */
const checkSelectOption = (text: string, key: number) => {<!-- -->
return checkedList[key] === text;
};
/* Complete all selections */
const onSelectCascaderFinish = (selectValues: Array<string>) => {<!-- -->
setOpen(false);
setRenderCheckedText(selectValues);
// Convert data to value form for export
const values = renderTextCallback(selectValues, allCascaderList, fileNames, 'value');
onFinish & amp; & amp; onFinish(values);
};
/* Click to clear information */
const handleClear = (e: any) => {<!-- -->
e. stopPropagation();
onSelectCascaderFinish([]);
setCheckedList([]);
};
/* close the checkbox */
const handleOpenChange = (newOpen: boolean) => {<!-- -->
setOpen(newOpen);
if (newOpen) {<!-- -->
let newTabList = configNav(checkedList, column);
setTabList(newTabList);
setActiveKey(checkedList. length ? checkedList. length - 1 : 0);
}
};

// Render a three-level selection list
const CascaderContext = () => (
<>
{<!-- -->/* ------- Hierarchical linkage navigation list ------- */}
<div className='cascader-tab-select cursor' onClick={<!-- -->(e) => e.stopPropagation()}>
{<!-- -->tabList. map((item, idx) => (
<div
onClick={<!-- -->() => onChange(idx)}
className={<!-- -->`cascader-tab-item ${<!-- -->activeKey === idx ? 'active' : ''} hover-color`}
key={<!-- -->'cascader-tab-item-' + idx}>{<!-- -->item}</div>
))}
</div>
{<!-- -->/* ----------- Layer linkage option display ------------- */}
<ul className='cascader-list-wrapper'>
{<!-- -->renderCallback(checkedList, activeKey)?.map((item: objTypes) => (
<li key={<!-- -->item.value} onClick={<!-- -->(e) => handleSelectCascader(e, item.text)}
className={<!-- -->`${<!-- -->checkSelectOption(item.text, activeKey) ? 'checked-cascader' : ''} cursor`}
>
{<!-- -->checkSelectOption(item.text, activeKey) ? <CheckOutlined className='checked-cascader-icon primary-color' /> : null}
<span>{<!-- -->item.text}</span>
\t\t\t\t\t</li>
))}
\t\t\t</ul>
</>
)

return (
<>
<Popover overlayClassName='cascader-popover-wrapper' content={<!-- --><CascaderContext />}
trigger="click" placement='bottom' open={<!-- -->open} arrow={<!-- -->false}
getPopupContainer={<!-- -->(triggerNode: any) => triggerNode?.parentNode}
onOpenChange={<!-- -->handleOpenChange}>
{<!-- -->render ? render(renderCheckedText) :
<Cascader allowClear open={<!-- -->false} value={<!-- -->renderCheckedText} options={<!-- -->undefined}
placeholder={<!-- -->placeholder || 'Please select'} displayRender={<!-- -->() => checkedList.join(separator)}
suffixIcon={<!-- --><CaretDownOutlined />} clearIcon={<!-- --><CloseCircleFilled onClick={<!-- -->handleClear} />}
className='cascader-render-input' style={<!-- -->{<!-- --> width: '100%' }} />
}
</Popover>
</>
)
}

export default memo(CascaderList);

less style:

.cascader-popover-wrapper {<!-- -->
    width: 100%;

    .ant-popover-inner {<!-- -->
        padding: 0;
        border-radius: 2px;
        border: 1px solid #e2e6ed;
        max-height: 300px;
        font-size: 12px;
        box-shadow: 0 4px 12px 0 rgba(56, 56, 56, .15);
    }
}

.cascader-tab-select {<!-- -->
    display: flex;
    align-items: center;
    height: 40px;
    line-height: 40px;
    padding-left: 36px;
    color: rgba(0, 0, 0, 0.65);
    border-bottom: 1px solid #f0f0f0;

    .cascader-tab-item {<!-- -->
        margin-right: 24px;

         & amp;:last-of-type {<!-- -->
            margin-right: 0;
        }
    }
}

.cascader-list-wrapper {<!-- -->
    max-height: 260px;
    padding: 0 4px;
    box-sizing: border-box;
    overflow: auto;

    li {<!-- -->
        position: relative;
        height: 30px;
        line-height: 30px;
        padding-left: 32px;
        color: #3d4757;
        margin: 4px 0;

         & amp;:not(.checked-cascader):hover {<!-- -->
            background-color: #f2f3f5;
        }

        .checked-cascader-icon {<!-- -->
            position: absolute;
            left: 10px;
            top: 10px;
        }
    }

    .checked-cascader {<!-- -->
        color: @primary-color;
        background-color: #f2f3f5;
    }
}

.cascader-render-input {<!-- -->
    border-radius: 2px;

     &:focus {<!-- -->
        box-shadow: none;
    }
}

Instructions:

//1. Import components
import CascaderList from '@/components/CascaderList';

//2. Use on the page
<CascaderList column={<!-- -->3} options={<!-- -->options} checkedValue={<!-- -->value} placeholder='Please select'
onFinish={<!-- -->onFinish} separator='/' fileNames={<!-- -->fileNames} render={<!-- -->render} />

// column represents the number of levels, the default is 3
// options represent hierarchical linkage data, such as the above json data
// checkedValue represents the selected data array, which is the value of value in the above json data
// separator represents the separator, the default is /
// fileNames represents the data configuration in the json data
/*
fileNames = {
text: 'text',
value: 'value',
children: 'children'
}
*/
// render is a callback function, the parameter is the currently selected text content, which can be used to customize the page display content
// onFinish is a function to complete the selection, and the parameter is the currently selected value content

Finally, you can copy the code into the project and use it. Compared with the traditional hierarchical linkage, the page display effect is better! It is not easy to develop, I hope to get the support of veterans, bookmark, like and share this is the motivation for us to share the source code!