Omi樹元件omi-tree編寫指南

【當耐特】發表於2017-05-02

Omi框架能夠以少量的程式碼宣告式地編寫可拖拽移動節點的樹形元件。
通常樹元件能夠考驗UI框架的健壯性,因為需要使用到UI框架的如下特性:

  • 元件巢狀
  • 元件傳值
  • 元件批量傳值
  • 元件依賴自身遞迴巢狀(nest-self)
  • 子、孫或炎黃子孫訪問根元件例項

下面來介紹下使用 omi-tree 的開發全過程。你也可以無視文章,先體驗一把和直接編輯原始碼試一把:

omi-tree playground

類劃分

  • tree.js 樹元件的根容器類,包含節點移動,根據id獲取節點等通用方法,這裡把其排除在tree-node之外
  • tree-node.js 樹節點,自遞迴巢狀元件,因為tree-node中可以包含tree-node

樹的資料規則:

{
    name: 'Root',
    children: [
        {
            name: 'A',
            id: 1,
            children: [
                { id: 4, name: 'A1', children: [] },
                { id: 7, name: 'A2', children: [] }
            ]
        },
        {
            name: 'B',
            id: 2,
            children: [
                { id: 5, name: 'B1', children: [] },
                { id: 8, name: 'B2', children: [] }
            ]
        },
        {
            name: 'C',
            id: 3, children: [
            { id: 6, name: 'C1', children: [] },
            { id: 9, name: 'C2', children: [] }
        ]
        }
    ]
}

可以看到,每個節點都有唯一的id來標識,每個節點也有children屬性來存放自己的子節點的資訊。

元件HTML結構

tree結構:

<ul>
  <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
</ul>
  • 通過 o-repeat 生成所有 tree-node
  • group-data 把 data.children 的資料批量傳遞給各個 tree-node

這裡需要特別注意的是:

  • o-repeat 等所有指令對應的 scope 資料是 this.data
  • group-data,data等等 的 scope 是 this

tree-node結構:

<li data-node-id="{{id}}"  draggable="true"  ondragstart="dragStartHandler" ondragleave="dragLeaveHandler"  ondrop="dropHandler" ondragover="dragOverHandler" >
    <div data-node-id="{{id}}">{{name}}</div>
    <ul data-node-id="{{id}}" o-if="children.length > 0">
        <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
    </ul>
</li>

可以看到每個tree-node都標記了draggable代表可以拖拽,drag和drop的支援情況大家可以caniuse一把。

  • 每個tree-node 既是拖拽對應,也是drop容器物件
  • li、div和ul都標記了 data-node-id 來存放id在dom元素上方便js裡讀取和傳遞

完整程式碼解析

先看tree:

class Tree extends Omi.Component {
    //移動節點
    moveNode(id, parentId) {
        if (id === parentId) {
            return
        }

        if(this.check(parentId, id)) {
            let parent = this.getChildById(parentId, this.data.children)
            let child = this.removeChildById(id, this.data.children)
            parent.children.push(child)
            this.update()
        }
    }
    //驗證子節點的孩子節點是否包含父親節點,這裡主要是為了防止把父節點拖拽到自己的孩子節點當中,這是個錯誤的邏輯操作
    check(parentId, childId){
        let current = this.getChildById(childId, this.data.children),
            children = current.children
        for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            if (child.id === parentId) {
                return false
            }

            let errorIds = this.check(parentId, child.id )
            if (!errorIds) {
                return false
            }
        }

        return true
    }
    //根據id移除child節點資料
    removeChildById(id, children) {

        for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            if (child.id === id) {
                children.splice(i, 1)
                return child
            }

            let target = this.removeChildById(id, child.children)
            if (target) {
                return target
            }

        }
    }
    //根據id獲取child節點資料
    getChildById(id, children) {
        for (let i = 0, len = children.length; i < len; i++) {
            let child = children[i]
            if (child.id === id) {
                return child
            }

            let target = this.getChildById(id, child.children)
            if (target) {
                return target
            }
        }
    }

    render() {
        return `<ul>
                  <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
              </ul>`
    }
}

//生成標籤用於宣告式嵌入其他元件
Omi.tag('tree', Tree)

下面來看 tree-node:

class TreeNode extends Omi.Component {

    dropHandler(evt) {
        //通過evt.dataTransfer.getData接收傳遞過來的資料
        this.getRootInstance(this.parent).moveNode(parseInt(evt.dataTransfer.getData("node-id")), parseInt(evt.target.dataset['nodeId']))
        this.node && this.node.classList.remove('drag-over')
        evt.stopPropagation()
        evt.preventDefault()

    }

    getRootInstance(parent){
        if(parent.moveNode){
            return parent
        }else{
            return this.getRootInstance(parent.parent)
        }

    }

    dragOverHandler(evt){
        this.node.classList.add('drag-over')
        evt.stopPropagation()
        evt.preventDefault()
    }

    dragLeaveHandler(){
        this.node.classList.remove('drag-over')
    }
    
    dragStartHandler(evt){
        //設定要傳遞的資料
        evt.dataTransfer.setData("node-id",this.data.id)
        evt.stopPropagation()
    }
    
    //區域性樣式,drag-over是拖拽在node之上的一個啟用樣式
    style(){
        return `
            .drag-over{
                border:1px dashed black;
            }
        `
    }

    render(){
        return `
                <li data-node-id="{{id}}"  draggable="true"  ondragstart="dragStartHandler" ondragleave="dragLeaveHandler"  ondrop="dropHandler" ondragover="dragOverHandler" >
                    <div data-node-id="{{id}}">{{name}}</div>
                    <ul data-node-id="{{id}}" o-if="children.length > 0">
                        <tree-node o-repeat="child in children" group-data="data.children"></tree-node>
                    </ul>
                </li>
            `
    }
}

//生成標籤用於宣告式嵌入其他元件
Omi.tag('tree-node',TreeNode)
  • dragStart的時候通過evt.dataTransfer.setData設定需要傳遞的資料,這裡存放了拖拽的節點id
  • drop的時候通過evt.dataTransfer.getData讀取傳遞過來的資料,這裡取drag的node的節點id
  • 通過 o-if="children.length > 0" 決定是否生成 ul 標籤
  • getRootInstance元件是遞迴去調取tree的物件的例項(因為tree-node可能包含tree-node,所以需要遞迴讀parent)
  • 拿到tree的例項之後,呼叫tree的物件的例項的moveNode方法去移動節點,moveNode的本質就是修改節點資料,然後update元件

到此位置,複雜的拖拽移動都完成了。增刪改查就更加簡單了,大家可以接著試試~~~

Omi相關

相關文章