<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
系統有個整改需求,要求系統內的所有表格支援本地動態列顯隱,拖拽排序列位置,固定列功能,涉及的頁面很多
上效果圖:
其實最開始想的肯定是json設定表單的形式,再由迴圈出來的列去控制對應的位置和屬性 但是!很多頁面啊!每個頁面都要去轉json設定意味著大量的工作量和極高的風險
能不能我就寫個自己的元件來包一層,這樣我就能實現最小改動的情況下只需要替換元件標籤來實現這個功能
與實際的不同只是我將原來的el-table換成了hf-table,同時支援原本el-table的所有功能
我們不可能去自己實現一個el-table的元件,所以無非我們的元件就是在el-table的基礎上套一層殼,給他加上一個設定按鈕,同時設定的內容能夠去影響整個表格的渲染。
那既然我們不自己實現el-table則意味著原先程式碼中的el-table-column我們要拿到,並且要傳給el-table,這樣我們才能去渲染出來原先的那個表格
在一個元件的範例中,我們能夠通過vnode去獲取到當前的一個虛擬dom,vnode去獲取到當前的一個虛擬dom,vnode去獲取到當前的一個虛擬dom,vnode有一個componentOptions元件設定項的屬性,通過他的children就能獲取到所有的el-table-column 虛擬dom陣列
上一步我們已經拿到了所有的el-table-column虛擬dom,那怎麼將虛擬dom去渲染成對應的表格元件呢?
這不render就該登場了嗎!!
這個children就是我們拿到的el-table-column的陣列,我們只需要將該虛擬dom的陣列以元件屬性的形式傳傳進來了,再建立一個el-table,將對應的children傳給他!臥槽,這不就又和原本<el-table>xxx</el-table>
的效果一毛一樣嗎,是的 ,我做的就是掛羊頭賣狗肉。
也就是說,實際上我的hf-table只是劫持了el-table,他的作用只是拿到原本寫的el-table-colunm的虛擬dom,去渲染成一個表格
此時我們的任務已經完成大半了,就是我原本el-table的標籤已經可以被替換了,那我們要做的就只剩下操作表格了。 實際我做的很簡單,既然我已經拿到了所有的子節點,那我就在hf-table元件中去操作成我想要的陣列,再丟給render函數去渲染就好了
整個元件的程式碼,程式碼量除掉樣式也就不到100行
<template> <div class="hf-table"> <el-popover placement="bottom-end" width="400" popper-class="table-cloumn-setting-popper" trigger="click" > <div class="setting-row-content"> <draggable v-model="storageList" handle=".el-icon-s-operation" @end="updateTable"> <div v-for="clo in storageList" :key="clo.label" class="setting-row"> <i class="el-icon-s-operation" /> <el-checkbox v-model="clo.show" class="label" @change="showOrHidden($event,clo)">{{ clo.label }}</el-checkbox> <el-button class="btn" size="mini" :type="clo.fixed === 'left' ? 'primary' : 'default'" @click="setFixed('left',clo)" >固定在左側</el-button> <el-button class="btn" size="mini" :type="clo.fixed === 'right' ? 'primary' : 'default'" @click="setFixed('right',clo)" >固定在右側</el-button> </div> </draggable> </div> <i slot="reference" class="el-icon-setting" /> </el-popover> <new-table v-if="showTable" :config="config" /> </div> </template> <script> import draggable from 'vuedraggable' import newTable from './table.js' const components = { newTable, draggable } export default { components, props: { storageName: { type: String, default: 'hfTable' } }, data() { return { showTable: false, storageList: [], name: '', config: { children: [], attrs: {}, listeners: {} } } }, watch: { '$attrs': { handler(newV) { this.$set(this.config, 'attrs', newV) }, deep: true, immediate: true } }, mounted() { this.initStorage() this.updateTable() }, methods: { showOrHidden(val, clo) { if (!val && this.storageList.filter(i => i.show).length === 0) { this.$message.warning('列表最少顯示一列') this.$nextTick(() => { clo.show = true }) return } this.updateTable() }, setFixed(value, clo) { if (clo.fixed === value) { clo.fixed = false } else { clo.fixed = value } this.updateTable() }, // 初始化快取設定 initStorage() { this.storageList = [] const storage = window.localStorage.getItem(this.storageName) // 不管是否初次還是要做一下處理,萬一頁面有修改,做一下更新,以最新的node節點陣列為準 let list = storage ? JSON.parse(storage) : [] this.$vnode.componentOptions.children.forEach(node => { // 以label為準,因為可能會改文字 if (!node.componentOptions.propsData.type && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) { // 不是特殊型別的 找不到就加上 const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData)) propsData.fixed = propsData.fixed !== undefined ? 'left' : false list.push({ fixed: false, // 預設新增的都是不固定 show: true, // 預設新增的都是顯示的 ...propsData }) } }) // 必須在節點陣列存在的才有意義 list = list.filter(item => this.$vnode.componentOptions.children.find(n => { return item.label === n.componentOptions.propsData.label })) this.storageList = list }, // 根據快取的陣列進行渲染表格 updateTable() { const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions.propsData.type) this.storageList.forEach(item => { if (item.show) { const node = this.$vnode.componentOptions.children.find(n => n.componentOptions.propsData.label === item.label) if (node) { node.componentOptions.propsData.fixed = item.fixed childrenNodes.push(node) } } }) this.config.children = childrenNodes this.config.attrs = this.$attrs this.config.listeners = this.$listeners this.showTable = false this.$nextTick(() => { this.showTable = true }) window.localStorage.setItem(this.storageName, JSON.stringify(this.storageList)) } } } </script> <style lang="scss" scoped> .table-cloumn-setting-popper{ .setting-row-content{ max-height: 600px; overflow-y: auto; .setting-row{ height: 40px; line-height: 40px; .el-icon-s-operation{ cursor: move; font-size: 16px; margin-right: 8px; } .label{ margin-right: 8px; } .btn{ padding: 4px!important; } } } } .hf-table{ width:100%; height:100%; position: relative; .el-icon-setting{ position: absolute; right: 20px; top:-20px; cursor: pointer; } } </style>
import Vue from 'vue' export default Vue.component('newtable', { functional: true, props: {}, listeners: {}, render: function(h, context) { return h( 'el-table', { props: context.data.attrs.config.attrs, on: context.data.attrs.config.listeners }, context.data.attrs.config.children ) } })
當真的推行到專案中時,發現了以上程式碼存在了幾個問題:
1.函數式元件沒有生命週期和範例,也就是table.js幫我們渲染了el-table,我們卻沒辦法拿到el-table 的範例,也就沒辦法去呼叫table原生的方法,例如clearSelection等
2.忘了做插槽傳遞。例如空資料自定義插槽等
<template> <div class="hf-table"> <el-popover placement="bottom-end" popper-class="table-cloumn-setting-popper" trigger="click" > <div class="setting-row-content"> <div style="text-align:right"> <el-button @click="delAllStorage">恢復系統表格設定</el-button> <el-button @click="delStorage">恢復當前表格設定</el-button> </div> <draggable v-model="storageList" handle=".el-icon-s-operation" @end="updateTable"> <div v-for="clo in storageList" :key="clo.label" class="setting-row"> <i class="el-icon-s-operation" /> <el-checkbox v-model="clo.show" class="label" @change="showOrHidden($event,clo)">{{ clo.label }}</el-checkbox> <el-button class="btn" size="mini" :type="clo.fixed === 'left' ? 'primary' : 'default'" @click="setFixed('left',clo)" >固定在左側</el-button> <el-button class="btn" size="mini" :type="clo.fixed === 'right' ? 'primary' : 'default'" @click="setFixed('right',clo)" >固定在右側</el-button> </div> </draggable> </div> <i slot="reference" class="el-icon-setting" /> </el-popover> <!-- 按鈕容器 --> <div class="table-operate-btn-content" > <!-- 插槽自定義表格上方操作欄 --> <slot name="operateBtnContent"> <!-- 預設左右都有操作按鈕,如果單純想左或者想右,請在插入具名插槽 --> <div class="operate-btn-content"> <!-- 流式左右佈局 --> <slot name="btnContentLeft"> <div /> </slot> <slot name="btnContentRight"> <div /> </slot> </div> </slot> </div> <div :style="{height:`${tableHeight}px`}"> <new-table v-if="showTable" :config="config" /> </div> </div> </template> <script> import draggable from 'vuedraggable' import newTable from './table.js' import setHeight from '@/mixins/setHeight' const components = { newTable, draggable } export default { name: 'HfTable', components, mixins: [setHeight], props: { storageName: { type: String, required: true } }, data() { return { showTable: false, storageList: [], name: '', config: { children: [], attrs: {}, listeners: {} } } }, watch: { '$attrs': { handler(newV) { this.$set(this.config, 'attrs', newV) }, deep: true, immediate: true } }, mounted() { this.initStorage() this.updateTable() }, methods: { getInstance() { const ref = this.$children.find(i => i.$options._componentTag === 'el-table') return ref }, delStorage() { this.$confirm('恢復當前表格設定將清除當前表格設定並重新整理頁面是否繼續?', '提示', { confirmButtonText: '確定', cancelButtonText: '取消', type: 'warning' }).then(() => { const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {} storage[this.storageName] = [] window.localStorage.setItem('tableStorage', JSON.stringify(storage)) location.reload() }) }, delAllStorage() { this.$confirm('恢復系統表格設定將清除當前表格設定並重新整理頁面是否繼續?', '提示', { confirmButtonText: '確定', cancelButtonText: '取消', type: 'warning' }).then(() => { window.localStorage.removeItem('tableStorage') location.reload() }) }, showOrHidden(val, clo) { if (!val && this.storageList.filter(i => i.show).length === 0) { this.$message.warning('列表最少顯示一列') this.$nextTick(() => { clo.show = true }) return } this.updateTable() }, setFixed(value, clo) { if (clo.fixed === value) { clo.fixed = false } else { clo.fixed = value } this.updateTable() }, // 初始化快取設定 initStorage() { this.storageList = [] const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {} // 不管是否初次還是要做一下處理,萬一頁面有修改,做一下更新,以最新的node節點陣列為準 let list = storage[this.storageName] ? storage[this.storageName] : [] this.$vnode.componentOptions.children.forEach(node => { // 以label為準,因為可能會改文字 if (!(!node.componentOptions || node.componentOptions.propsData.type) && list.findIndex(i => i.label === node.componentOptions.propsData.label) < 0) { // 非插槽且 不是特殊型別的 找不到就加上 const propsData = JSON.parse(JSON.stringify(node.componentOptions.propsData)) if (propsData.fixed === undefined || propsData.fixed === false) { propsData.fixed = false } else { propsData.fixed = propsData.fixed ? propsData.fixed : 'left' } list.push({ fixed: false, // 預設新增的都是不固定 show: true, // 預設新增的都是顯示的 ...propsData }) } }) // 必須在節點陣列存在的才有意義 list = list.filter(item => this.$vnode.componentOptions.children.find(n => { return n.componentOptions && item.label === n.componentOptions.propsData.label })) this.storageList = list }, // 根據快取的陣列進行渲染表格 updateTable() { // 特殊型別 const childrenNodes = this.$vnode.componentOptions.children.filter(node => node.componentOptions && node.componentOptions.propsData.type) this.storageList.forEach(item => { if (item.show) { const node = this.$vnode.componentOptions.children.find(n => n.componentOptions && n.componentOptions.propsData.label === item.label) if (node) { node.componentOptions.propsData.fixed = item.fixed childrenNodes.push(node) } } }) this.config.children = childrenNodes this.config.attrs = this.$attrs this.config.listeners = this.$listeners this.showTable = false this.$nextTick(() => { this.showTable = true }) const storage = window.localStorage.getItem('tableStorage') ? JSON.parse(window.localStorage.getItem('tableStorage')) : {} storage[this.storageName] = this.storageList window.localStorage.setItem('tableStorage', JSON.stringify(storage)) } } } </script> <style lang="scss" scoped> .table-cloumn-setting-popper{ .setting-row-content{ max-height: 600px; overflow-y: auto; .setting-row{ height: 40px; line-height: 40px; .el-icon-s-operation{ cursor: move; font-size: 16px; margin-right: 8px; } .label{ margin-right: 8px; } .btn{ padding: 4px!important; } } } } .hf-table{ width:100%; height:100%; position: relative; .el-icon-setting{ position: absolute; right: 10px; top:16px; cursor: pointer; } .table-operate-btn-content{ width: calc(100% - 40px); .operate-btn-content { height: 40px; display: flex; justify-content: space-between; align-items: center; } } } </style>
針對插槽的處理主要是根據插槽沒有componentOption屬性,然後把它和帶有type的這類vnode直接丟給el-table,而其他的再去做顯隱的處理。
import Vue from 'vue' export default Vue.component('newtable', { functional: true, props: {}, listeners: {}, render: function(h, context) { const scopedSlots = {} Object.keys(context.parent.$scopedSlots).forEach(key => { if (key !== 'default') { scopedSlots[key] = context.parent.$scopedSlots[key] } }) return context.parent.$createElement( 'el-table', { props: { ...context.data.attrs.config.attrs, ref: 'newtable' }, on: context.data.attrs.config.listeners, attrs: { ref: 'newtable' }, scopedSlots }, context.data.attrs.config.children ) } })
針對函數式元件沒有範例的問題,這裡我直接呼叫了父級元件的$createElement
方法去建立el-table,再利用父級元件的children中‘children中`children中‘options._componentTag === 'el-table'`的vnode,來拿到對應的範例
有點奇怪的是我在建立的時候給生成的元件設定attrs的ref,在父元件中$refs無法拿到
還有一點要注意!我在控制元件重新渲染的時候,使用了$nexttick
,所以不要在勾點函數中使用getInstance()
方法獲取表格元件範例,如果一定要,那就用鏈式判一下空再用this.$refs.hftable.getInstance()?.xxx()
其實這是不是的合適方案也未定,但是最主要是過程中的一個探索吧。包括評論說的$slots
去繼承所有的vnode節點。其實在拿不到表格範例的時候我想換方案的時候嘗試過這個辦法,我直接去掉了函數式元件。直接寫了一個el-table,然後具名插槽去接收。但是因為要操作vnode,也就是我要隱藏某列,這意味著我需要去修改hf-table中$slots
中的default陣列,就總覺得不太合適,共同進步吧~更多關於表格函數式元件封裝element的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45