Omi框架能夠以少量的程式碼宣告式地編寫可拖拽移動節點的樹形元件。
通常樹元件能夠考驗UI框架的健壯性,因為需要使用到UI框架的如下特性:
- 元件巢狀
- 元件傳值
- 元件批量傳值
- 元件依賴自身遞迴巢狀(nest-self)
- 子、孫或炎黃子孫訪問根元件例項
下面來介紹下使用 omi-tree 的開發全過程。你也可以無視文章,先體驗一把和直接編輯原始碼試一把:
類劃分
- 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相關
- Omi官網omijs.org
- Omi的Github地址https://github.com/AlloyTeam/omi
- 如果想體驗一下Omi框架,可以訪問 Omi Playground
- 如果想使用Omi框架或者開發完善Omi框架,可以訪問 Omi使用文件
- 如果你想獲得更佳的閱讀體驗,可以訪問 Docs Website
- 如果你懶得搭建專案腳手架,可以試試 omi-cli
- 如果你有Omi相關的問題可以 New issue
- 如果想更加方便的交流關於Omi的一切可以加入QQ的Omi交流群(256426170)