插件组件封装系列:tree插件封装

1,051 阅读3分钟

本文所有完整代码-github

前端开发中将后台返回的层级结构转化为可视的树形图,并且可以进行层级的折叠展开操作是一个常见的需求。本文来实现一个tree样式的树形图组件。

demo实现

原理

核心就是根据数据递归构造树形DOM+事件委托给折叠/展开的图标

  1. 根据数据递归构造树形DOM 数据结构:

    [
      {
        "name": "动物动物",
        "open": true,
        "children": [
          {
            "name": "水生动物",
            "children": [
              {
                "name": "海洋动物"
              }
            ]
          }
        ]
      }
    ]
    

    image。png 每一条数组数据都是一个 <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>
    
  2. 事件委托给折叠/展开的图标 折叠/展开图标使用事件委托,委托给最外层的容器

完整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();

封装成插件

代码

本文所有完整代码-github

(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文件夹下查看例子完整代码

查看例子-gitPage

<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>

本文所有完整代码-github