目录
最近要做一个只用原生js递归制作一个树形结构的小dom
记录一下写dom的过程
1.分析数据结构
根据接口,我这边的数据结构类型是这样的
let data = [{
child: [{
child: [{
child: [],
id: 3,
mobile: "18800000008",
username: "GWCQ444"
}],
id: 2,
mobile: "18800000007",
username: "GWCQ555"
}],
id: 1,
mobile: "18800000006",
username: "GWCQ666"
}]
很典型的需要递归拆解的数组结构,child里没有元素的时候是空数组
2.原生js数据请求
原生js的数据请求方式是原生的阿贾克斯,也就是XMLHttpRequest()这个东西
渲染界面首先要拿到数据,所以js一开始就要做数据请求
let xhr = new XMLHttpRequest();
let data = []
xhr.open('GET', blobUrl, true); //blobUrl是请求接口
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
data = JSON.parse(xhr.responseText).data;
}
}
因为所有对数据的操作都是在这个请求之后拿到数据才能进行的,所以所有操作都要写在请求成功之后,就是xhr.status == 200 这个判断成功之后,不是很美观,小封装一下代码(当然也可以不封装,如果请求接口少的话也无所谓封装不封装了)
封装成函数
function getUserData(blobUrl) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('GET', blobUrl, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
resolve(JSON.parse(xhr.responseText).data);
}
}
});
}
利用一个Promise进行封装,这样写起来就看着不至于冗杂了
然后完成第一步,拿到数据
3.递归算法(关键)
拿到数据之后就是经典递归算法
这里用ul 和 li 渲染数据
<div class='lists'></div>
.lists {
margin-top: 20px;
}
li {
margin-bottom: 5px;
border-left: 1px solid #000;
cursor: pointer; /*禁止选中*/
padding: 10px 0;
}
直接上递归算法
function tree(data) {
var str = "<ul style='padding-left:20px;'>";
for (var i = 0; i < data.length; i++) {
// str += "<li>" + data[i].username;
str += "<li style='display:none' id=" + data[i].id + ">" + data[i].username + ' id=' + data[i].id;
if (data[i].child) {
str += tree(data[i].child)
}
str += "</li>";
}
str += "</ul>";
return str;
}
递归算法的核心在于,如果data[i].child存在元素,则说明树叉下面还能继续分,则调用函数本身,继续往下分
这里给li绑定id,可以做查询展开之后的锚点定位
document.querySelector(".lists").innerHTML = tree(res);
一行代码渲染上去
到这里这个dom的主体就这样完成了,剩下的就是小功能拓展
4.一键展开/隐藏
刚开始的版本是想简单做一个一键展开或者隐藏的操作,但是觉得功能单一,还是展开和隐藏的按键分开做比较好,因为我想的是后面如果做搜索,可以直接调用全部展开的按键就行了
直接上最终版本
<button class="btn_1">
全部展示
</button>
<button class="btn_2">
全部隐藏
</button>
let item_li = document.getElementsByTagName('li')
item_li[0].style.display = 'block'
//按钮
let btn_1 = document.querySelector('.btn_1')
let btn_2 = document.querySelector('.btn_2')
btn_1.onclick = function () {
for (let i = 0; i < item_li.length; i++) {
item_li[i].style.display = 'block'
let ul_list = item_li[i].children
for (let i = 0; i < ul_list[0].children.length; i++) {
if (ul_list[0].children[i].children[0].children.length === 0) {
ul_list[0].children[i].style.color = 'red'
}
}
}
}
btn_2.onclick = function () {
for (let i = 0; i < item_li.length; i++) {
item_li[i].style.display = 'none'
}
item_li[0].style.display = 'block'
}
最后如果li不能展开,就把li的字颜色改为红色便于判断
这里解释一下为什么要用双循环,因为li的下面第一个子元素的ul,所以得把这个li下面的ul的li集合单独拿出来,到最后一个ul的时候,下面的children为空数组
一件隐藏就保持第一行li显示
5.单独点击没一个树杈,能做到展开/隐藏下面的子元素
第一步,给所有里都批量绑定点击事件
第二步,点击显示/隐藏自己的子元素,依靠子元素的ul是否显示来进行判断什么时候需要显示/隐藏
第三步,直接上代码!
for (let i = 0; i < item_li.length; i++) {
//批量绑定点击事件
item_li[i].onclick = function (e) {
let ul_list = item_li[i].children
//无展开列表提示背景
for (let i = 0; i < ul_list[0].children.length; i++) {
if (ul_list[0].children[i].children[0].children.length === 0) {
ul_list[0].children[i].style.color = 'red'
}
}
//如果是最后一个,则不隐藏
if (ul_list[0].children.length) {
if (ul_list[0].children[0].style.display == 'none') {
operateList(ul_list[0].children, 'block')
} else {
operateList(ul_list[0].children, 'none')
}
}
//防止点击穿透
event.stopPropagation();
}
}
function operateList(arr, status) { // arr => 数组 status => 展开/隐藏
for (let i = 0; i < arr.length; i++) {
arr[i].style.display = status
}
}
展开和隐藏只有两种状态,做了一个小封装看着些许顺眼
6.完成搜索框(核心仍为递归算法)
多的不说,先画图
<div class="inp">
<input type="text" id="inp">
<button id="search">搜索</button>
</div>
<div class="title">
<span id="sp_show" style="display: none;">不存在该用户</span>
</div>
扁平化原数据,递归算法,启动!
//扁平化
function flat(data) {
let arr = [...data]
for (let i = 0; i < data.length; i++) {
if (data[i].child.length) {
arr.push(...flat(data[i].child))
} else {
arr.push(data[i])
}
}
return arr
}
因为后面要做锚点定位,再加一个事件点击的函数
function triggerEvent(e, eName) {
if (typeof e[eName] == 'function') {
e[eName]();
} else if (e.fireEvent) {
e.fireEvent('on' + eName);
} else if (document.createEvent) {
var evt = document.createEvent("MouseEvents");
evt.initEvent(eName, true, true);
e.dispatchEvent(evt);
}
}
然后就是获取输入框的值,利用扁平化过后的数据挨着查就行了,完整代码附上
//锚点定位
let input = document.getElementById('inp')
let a_btn = document.getElementById('search')
let sp_show = document.getElementById('sp_show')
a_btn.onclick = function () {
//判断id、名字、电话是否在列表中
sp_show.style.display = 'none'
let value = input.value
let flag = false
let id = null
for (let i = 0; i < userList.length; i++) {
if (userList[i].id == value) {
id = userList[i].id
flag = true
}
if (userList[i].username == value) {
flag = true
id = userList[i].id
}
if (userList[i].mobile == value) {
flag = true
id = userList[i].id
}
}
if (!flag) {
sp_show.style.display = 'inline-block'
} else {
triggerEvent(btn_1, 'click')
let a = document.createElement('a')
a.setAttribute('href', '#' + id)
a.setAttribute('style', "display:none")
triggerEvent(a, 'click')
for (let i = 0; i < item_li.length; i++) {
if (item_li[i].id == id) {
item_li[i].style.backgroundColor = '#000'
setTimeout(() => {
item_li[i].style.backgroundColor = '#eee'
}, 2000)
}
}
}
}