In recent work, there was a requirement that the business side did not want the table to scroll partially, but also wanted the table head to be fixed at the top of the page while scrolling along the page. But the Table component in ElementUI does not achieve this effect. There is no way but to directly code, simple implementation of the following effect. For the convenience of later reference. And to deepen the understanding of Vue custom instructions. I use custom instructions to do this. Considering that the parent element is different, the code is compatible with both #document scrolling and div scrolling.
The code has been uploaded to gitHub repository address
Use custom instructions
- We add a custom directive to each table that we want to fix the header
v-sticky
, and we need to pass in two parameters of the custom instruction,top
: Specifies the height from the top,parent
: specifies the scroll container if the scroll container is#document
, is not passed inparent
;
v-sticky="{
top:0,
parent:'#table_box'
}"
Copy the code
- Start writing custom instructions
- The code logic is written in comments
import Vue from 'vue'
// Set the style for the fixed header
function doFix(dom, top) {
dom.style.position = 'fixed'
dom.style.zIndex = '2001'
dom.style.top = top + 'px'
dom.parentNode.style.paddingTop = top + 'px'
}
// Unstyle the fixed header
function removeFix(dom) {
dom.parentNode.style.paddingTop = 0
dom.style.position = 'static'
dom.style.top = '0'
dom.style.zIndex = '0'
}
// Add class to the fixed header
function addClass(dom, fixtop) {
const old = dom.className
if(! old.includes('fixed')) {
dom.setAttribute('class', old + ' fixed')
doFix(dom, fixtop)
}
}
// Remove class from the fixed head
function removeClass(dom) {
const old = dom.className
const idx = old.indexOf('fixed')
if(idx ! = = -1) {
const newClass = old.substr(0, idx - 1)
dom.setAttribute('class', newClass)
removeFix(dom)
}
}
// The main function that determines whether the header is fixed
function fixHead(parent, el, top) {
/** * myTop The height of the current element from the scroll parent, ** fixTop The absolute height of the current element to be set * parentHeight The height of the scroll parent */
let myTop, fixtop, parentHeight
// Table header DOM node
const dom = el.children[1]
if (parent.tagName) {
// Local scrolling in the DOM
// The height of the current element from the scrolling parent = the height of the current element from the parent - the scrolling distance of the parent - the height of the header
myTop = el.offsetTop - parent.scrollTop - dom.offsetHeight
// Height of the parent element
const height = getComputedStyle(parent).height
parentHeight = Number(height.slice(0, height.length - 2))
// Absolute positioning height = height of the scrolling parent container relative to the viewport + height of the incoming suction top
fixtop = top + parent.getBoundingClientRect().top
// If you are farther from the top than the height of the parent element, that is, if you have not yet rolled out of the parent element, return
if (myTop > parentHeight) {
return}}else {
// the document node rolls
// Height of current element from scrolling parent = distance of current element from top of viewport
myTop = el.getBoundingClientRect().top
// Parent height = viewport height
parentHeight = window.innerHeight
// Absolute positioning height = incoming ceiling height
fixtop = top
// If you are farther from the top than the height of the parent element, that is, if you have not yet rolled out of the parent element, return
if (myTop > document.documentElement.scrollTop + parentHeight) {
return}}// If the scrollup is not displayed in the parent container. Direct return
if (Math.abs(myTop) > el.offsetHeight + 100) {
return
}
if (myTop < 0 && Math.abs(myTop) > el.offsetHeight) {
// If the current table has been fully scrolled over the parent element, it is not displayed on the parent element. The fixed position needs to be removed
removeClass(dom)
} else if (myTop <= 0) {
// If the header is rolled to the top of the parent container. Fixed position
addClass(dom, fixtop)
} else if (myTop > 0) {
// If the table scrolls up and into the parent container. Canceling fixed Positioning
removeClass(dom)
} else if (Math.abs(myTop) < el.offsetHeight) {
// If the absolute value of the scrolling distance is less than its own height, that is, if the table is scrolling up, the end of the table needs to be fixed
addClass(dom, fixtop)
}
}
// Set the width of the container outside the table header to the width of the table body when the header is fixed
function setHeadWidth(el) {
// Get the width of the body of the current table
const width = getComputedStyle(
el.getElementsByClassName('el-table__body-wrapper') [0]
).width
// Set the width of the table. By default, multiple tables on a page are the same width. So you can go through the assignment directly, or you can set it separately, depending on your needs
const tableParent = el.getElementsByClassName('el-table__header-wrapper')
for (let i = 0; i < tableParent.length; i++) {
tableParent[i].style.width = width
}
}
/** * there are three global objects. Used to store listening events. Facilitates removal of listening events */ after component destruction
const fixFunObj = {} // Hold the listener scroll event for the scroll container
const setWidthFunObj = {} // Used to store the event of recalculating the head width after page resize
const autoMoveFunObj ={} // The header of the fix layout needs to scroll up with the document when the document is scrolled locally within a DOM element
// Globally register custom events
Vue.directive('sticky', {
// When the bound element is inserted into the DOM...
inserted(el, binding, vnode) {
// Set the width of the header
setHeadWidth(el)
// Get the ID of the current vueComponent. As a key to store various listening events
const uid = vnode.componentInstance._uid
// Recalculate the header width when window resize and store the listener function in the listener object to remove the listener event
window.addEventListener(
'resize',
(setWidthFunObj[uid] = () = > {
setHeadWidth(el)
})
)
// Get what the current scrolling container is. If I scroll document. By default, the parent parameter is not passed
const scrollParent =
document.querySelector(binding.value.parent) || document
// Add scroll listener to the scroll container. And the listener function is stored in the listener function object, easy to remove the listener event
scrollParent.addEventListener(
'scroll',
(fixFunObj[uid] = () = > {
fixHead(scrollParent, el, binding.value.top)
})
)
// Scroll inside a local DOM element. You need to listen for document scrolling, and document scrolling is synchronous scrolling of the table heads together. And the listener function is stored in the listener function object, easy to remove the listener event
if (binding.value.parent) {
document.addEventListener('scroll', autoMoveFunObj[uid] = () = > {
// Get the header DOM node
const dom = el.children[1]
// If the current table header is fixed. It rolls along with the document scroll
if(getComputedStyle(dom).position=== 'fixed') {// The rolling distance is: the height of the rolling parent container from the top of the viewport + the fixed distance of the incoming suction top
const fixtop =
binding.value.top + scrollParent.getBoundingClientRect().top
doFix(dom, fixtop, 'fixed')}})}},// Component updated. Recalculate the table header width
componentUpdated(el) {
setHeadWidth(el)
},
// Remove all listener events when the node is unbound.
unbind(el, binding, vnode) {
const uid = vnode.componentInstance._uid
window.removeEventListener('resize', setWidthFunObj[uid])
const scrollParent =
document.querySelector(binding.value.parent) || document
scrollParent.removeEventListener('scroll', fixFunObj[uid])
if (binding.value.parent) {
document.removeEventListener('scroll', autoMoveFunObj[uid])
}
}
})
Copy the code
Adding test code
- The first is the HTML code
<div class="table">
<div id="table_box" class="table_box">
<el-table
v-for="item in [1, 2]"
:key="item"
ref="stickyTable"
v-sticky="{ top: 0, parent: '#table_box' }"
:data="tableData"
style="width: 100%"
border
>
<el-table-column prop="date" :label="` date ${item} `" width="180">
</el-table-column>
<el-table-column prop="name" :label="` name ${item} `" width="180">
</el-table-column>
<el-table-column prop="address" :label="` address ${item} `">
</el-table-column>
</el-table>
</div>
<el-table
v-for="item in [3, 4]"
:key="item"
ref="stickyTable"
v-sticky="{ top: 0 }"
:data="tableData"
style="width: 100%"
border
>
<el-table-column prop="date" :label="` date ${item} `" width="180">
</el-table-column>
<el-table-column prop="name" :label="` name ${item} `" width="180">
</el-table-column>
<el-table-column prop="address" :label="` address ${item} `"> </el-table-column>
</el-table>
</div>
Copy the code
Four tables are defined in the page, the first two scrolling in a parent container div#table_box, and the last two scrolling with document
- Style the table to make it easier to distinguish each table from its header
.table { width: 100%; border: 1px solid #ddd; padding: 10px 20px; .table_box { border: 1px solid red; margin-bottom: 20px; height: 200px; overflow-x: hidden; overflow-y: auto; } .el-table { margin-bottom: 50px; border: 1px solid transparent; } /deep/ .el-table__header-wrapper { th { background: rgba(244, 244, 244, 1); }}}Copy the code
- Add some JS to add data to the table. use
setTimeOut
Simulate an asynchronous request for data
export default {
data() {
return {
tableData: []}},mounted() {
this.setTableData()
},
methods: {
setTableData() {
const result = []
for (let i = 0; i < 20; i++) {
result.push({
date: '2016-05-03'.name: 'Wang Xiaohu' + i,
address: Lane 1516, Jinshajiang Road, Putuo District, Shanghai + i
})
}
setTimeout(() = > {
this.tableData = result
}, 500)}}}Copy the code
The demo only supports simple tables in ElementUI. If there is a fixed left and right table layout, the style may be confused. For those of you who are interested, dig a little deeper