前端开发中将后台返回的层级结构转化为可视的树形图,并且可以进行层级的折叠展开操作是一个常见的需求。本文来实现一个tree样式的树形图组件。
demo实现
原理
核心就是根据数据递归构造树形DOM+事件委托给折叠/展开的图标
-
根据数据递归构造树形DOM 数据结构:
[ { "name": "动物动物", "open": true, "children": [ { "name": "水生动物", "children": [ { "name": "海洋动物" } ] } ] } ]每一条数组数据都是一个
<ul>标签,数组中的item就是一个<em>标签加上<li>标签。每一个item中,遇到的children都是一个新的<ul>标签,递归实现最终结构如下:
<ul class="level level0"> <li> <a href="#" class="title">动物动物</a> <em class="icon open"></em> <ul class="level level1" style="display:block"> <li> <a href="#" class="title">水生动物</a> <em class="icon open"></em> <ul class="level level2" style="display: block;"> ... </ul> </li> </ul> </li> </ul> -
事件委托给折叠/展开的图标 折叠/展开图标使用事件委托,委托给最外层的容器
完整demo代码:
<style>
.container {
box-sizing: border-box;
margin: 20px auto;
padding: 10px;
width: 600px;
border: 1px dashed #AAA;
-webkit-user-select: none;
}
.level {
display: none;
font-size: 14px;
margin-left: 10px;
}
.level.level0 {
display: block;
margin-left: 0;
}
.level li {
position: relative;
padding-left: 15px;
line-height: 30px;
}
.level li .icon {
position: absolute;
left: 0;
top: 9px;
box-sizing: border-box;
width: 12px;
height: 12px;
line-height: 8px;
text-align: center;
border: 1px solid #AAA;
background: #EEE;
cursor: pointer;
}
.level li .icon:after {
display: block;
content: "+";
font-size: 12px;
font-style: normal;
}
.level li .icon.open:after {
content: "-";
}
.level li .title {
color: #000;
}
</style>
<body>
<div class="container" id="container1">
<ul class="level level0">
</ul>
</div>
<script src="./ztree.js"></script>
</body>
ztree.js
//ztree.js
let zTreeModule = (function () {
let container = document.querySelector('.container'),
levelBox = document.querySelector('.container>ul');
// 从服务器获取数据
const queryData = () => {
return new Promise(resolve => {
let xhr = new XMLHttpRequest;
xhr.open('GET', './data.json');
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && (xhr.status >= 200 && xhr.status < 300)) {
let data = JSON.parse(xhr.responseText);
resolve(data);
}
};
xhr.send(null);
});
};
// 数据绑定
const bindHTML = data => {
let n = 0;
const gethtml = data => {
let str = `` ;
n++;//level 的层级处理,每深入一层,就++一次
data.forEach(item => {
let {
name,
children,
open
} = item;
str += `<li>
<a href="#" class="title">${name}</a>
${children && children.length>0?`
<em class="icon ${open?'open':''}"></em>
<ul class="level level${n}" style="display:${open?'block':'none'}">
${gethtml(children)}
</ul>
`: `` }
</li>` ;
});
n--;//回归一层就--
return str;
};
levelBox.innerHTML = gethtml(data);
};
// 基于事件委托实现点击折叠切换
const handle = () => {
container.addEventListener('click', function (ev) {
let target = ev.target;
if (target.tagName === 'EM') {
let ulBox = target.nextElementSibling,
isOpen = target.classList.contains('open');
if (!ulBox) return;
if (isOpen) {
ulBox.style.display = 'none';
target.classList.remove('open');
return;
}
ulBox.style.display = 'block';
target.classList.add('open');
}
});
};
return {
async init() {
let data = await queryData();
bindHTML(data);
handle();
}
};
})();
zTreeModule.init();
封装成插件
代码
(function () {
// 插件核心:OOP的模式,可以创建单独的实例,这样实现私有属性和公有方法的有效管理
class zTree {
constructor(element, options) {
// init params
let self = this,
len = arguments.length,
config = {//声明默认的参数
element: null,
data: null,
callback: function () {}
};
//通过判断参数长度,来完成两种不同的调用方式
if (len === 0) throw new Error( ` element or options are required! ` );
if (len === 1) options = element;
if (!options || typeof options !== "object") throw new TypeError( `options must be an object! ` );
if (len === 2) options.element = element;
//合并配置
config = Object.assign(config, options);
//校验参数格式进
// verify the format of the transmitted information
let {
element: element2,
data,
callback
} = config;
if (!element2 || element2.nodeType !== 1) throw new TypeError( ` element must be a DOM element object ` );
if (!Array.isArray(data)) throw new TypeError( ` data must be an array! ` );
if (typeof callback !== "function") throw new TypeError( ` callback must be an function! ` );
//mount to instance
//把config挂载到当前实例上
self.element = element2;
self.data = data;
self.callback = callback;
self.n = 0;
// init
self.init();
}
init() {
let self = this;
self.element.innerHTML = ` <ul class="level level0">
${self.html(self.data)}
</ul> ` ;
self.handle();
}
// dynamic creation of DOM structure
//递归创建DOM结构
html(data) {
let self = this,
str = `` ;
self.n++;
data.forEach(item => {
let {
name,
children,
open
} = item;
str += `<li>
<a href="#" class="title">${name}</a>
${children && children.length>0?`
<em class="icon ${open?'open':''}"></em>
<ul class="level level${self.n}" style="display:${open?'block':'none'}">
${self.html(children)}
</ul>
` : `` }
</li>` ;
});
self.n--;
return str;
}
// achieve specific functions
//使用事件委托进行事件绑定
handle() {
let self = this;
self.element.addEventListener('click', function (ev) {
let target = ev.target;
if (target.tagName === 'EM') {
let ulBox = target.nextElementSibling,
isOpen = target.classList.contains('open');
if (!ulBox) return;
if (isOpen) {
ulBox.style.display = 'none';
target.classList.remove('open');
self.callback(self, target, ulBox);
return;
}
ulBox.style.display = 'block';
target.classList.add('open');
self.callback(self, target, ulBox);
}
});
}
}
// 暴露API
if (typeof window !== "undefined") {
window.zTree = zTree;
}
//支持两种导入规范
if (typeof module === "object" && typeof module.exports === "object") {
module.exports = zTree;
}
})();
使用
引入:
<script src="dist/tree.min.js"></script>
也支持commonjs和es6module规范
支持自定义配置: options:
element:[dom]
data:[array]
callback:[function->self/em/ul]
两种使用方式:
new zTree([element],[options])
new zTree([options])
例如:
new zTree(container1, {
data
});
new zTree({
element: container2,
data,
callback(self, em, ul) {
console.log(self, em, ul);
}
});
例子
可以去仓库 example文件夹下查看例子完整代码
<div class="container" id="container1"></div>
<div class="container" id="container2"></div>
<script>
//...
(async function () {
const data = await queryData();
let container1 = document.querySelector('#container1'),
container2 = document.querySelector('#container2');
new zTree(container1, {
//data:data
data
});
new zTree({
element: container2,
data,
callback(self, em, ul) {
console.log(self, em, ul);
}
});
})();
</script>