Implementing Dynamic Columns in an Element UI Table with Vue
This guide shows how to create an Element UI table in Vue whose columns are generated from a configurable array, using colgroup/col for layout, v‑for loops for headers and rows, sticky left/right columns, edit‑mode inputs, and dynamic scroll‑shadow effects for a flexible, high‑performance table component.
This article demonstrates how to build a table component whose columns are generated dynamically (the number of columns is not fixed) using Element UI's table component together with Vue.
The data is divided into two arrays: columns (the header definition) and data (the row records). Each column object contains properties such as title (header text), dataIndex (the key used to fetch cell data), width , and optionally fixed for sticky columns. The row objects contain matching keys; the presence of a key in the row determines whether that column is displayed.
columns: [ // header definition { title: 'Full Name', width: 132, dataIndex: 'name', fixed: 'left' }, { title: 'Age', width: 100, dataIndex: 'age' }, { title: 'address1', dataIndex: 'address1', key: '1', width: 150 }, { title: 'address2', dataIndex: 'address2', key: '2', width: 150 }, // ... { title: '操作', dataIndex: 'do', width: 172, fixed: 'right' } ]; data: [ { key: 1, name: '章三', age: '18', class: '2班', address1: '111', address2: '222', address3: '333', address4: '444', address5: '555', address6: '666', address7: '777', isEdit: false }, { key: 2, name: '章三2', age: '18', class: '2班', address1: '111', address2: '222', address3: '333', address4: '444', address5: '555', address6: '666', address7: '777', isEdit: false } ];
In the Vue template the component uses v‑for to iterate over columns for generating <col> elements (inside a <colgroup> ) and over data for rows. The header cell displays {{ column.title }} , while each body cell renders {{ row[column.dataIndex] }} . When a row is in edit mode ( row.isEdit ), an a‑input or a custom component is shown instead of plain text.
The basic implementation relies on native table , tr and td elements. The header is placed inside a regular td (instead of th ) to allow flexible styling. The following template illustrates the core structure:
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
<table>
<colgroup>
<col v‑for="(column, index) in columns" :key="index"
:style="{ width: column.width + 'px', minWidth: column.width + 'px' }"
:class="{ 'fixed-left': index === 0, 'fixed-right': index === columns.length - 1 && column.fixed === 'right' }">
</colgroup>
<tbody>
<tr>
<td v‑for="(column, index) in columns" :key="index"
:class="{ 'fixed-left': index === 0, 'fixed-right': index === columns.length - 1 && column.fixed === 'right', 'header-cell': true }">
<div class="fixed-item">
<div style="display:flex;align-items:center;height:22px;line-height:22px;">{{ column.title }}</div>
</div>
</td>
</tr>
<tr v‑for="(row, rowIndex) in data" :key="rowIndex">
<td v‑for="(column, columnIndex) in columns" :key="columnIndex"
:class="{ 'fixed-left': columnIndex === 0, 'fixed-right': columnIndex === columns.length - 1 && column.fixed === 'right' }">
<div class="fixed-item">
<template v‑if="column.dataIndex === 'do'">
<div style="display:flex;align-items:center;height:22px;line-height:22px;">
<slot :row="row"></slot>
</div>
</template>
<template v‑else‑if="!row.isEdit && !row.component">
<div style="display:flex;align-items:center;height:22px;line-height:22px;">{{ row[column.dataIndex] }}</div>
</template>
<component :is="row.component" v‑bind="row.props" v‑else‑if="row.component"/>
<template v‑else>
<div style="display:flex;align-items:center;">
<a‑input v‑model="row[column.dataIndex]" placeholder="" allow‑clear/>
</div>
</template>
</div>
</td>
</tr>
</tbody>
</table>
</div>The accompanying CSS makes the first and last columns sticky using position: sticky , defines visual styles for header cells, and adds shadow effects that appear when the table is scrolled horizontally.
.table-container {
overflow-x: auto;
max-width: 100%;
position: relative;
td {
padding: 0;
background-color: #fff;
border-bottom: 0.9px solid #eee;
.fixed-item {
padding: 13px;
&.header-cell {
font-size: 14px;
color: rgba(0,0,0,0.85);
font-weight: 500;
}
}
}
}
.fixed-left {
position: sticky;
left: 0;
width: 142px;
align-items: center;
z-index: 9;
.fixed-item { display: block; }
}
.fixed-right {
position: sticky;
right: 0;
width: 172px;
align-items: center;
z-index: 9;
.fixed-item { display: block; }
}
.header-cell { background-color: #fafafa !important; }A handleScroll method listens to the table’s scroll event and toggles scroll-left / scroll-right classes to control the visibility of the side shadows.
handleScroll(event) {
const container = event.target;
const scrollLeft = container.scrollLeft;
const maxScrollLeft = container.scrollWidth - container.clientWidth;
// Add or remove shadow classes based on scroll position
if (scrollLeft === 0) {
container.classList.add('scroll-left');
container.classList.remove('scroll-right');
} else if (scrollLeft >= maxScrollLeft) {
container.classList.add('scroll-right');
container.classList.remove('scroll-left');
} else {
container.classList.add('scroll-left');
container.classList.add('scroll-right');
}
}Using <colgroup> and <col> lets the browser compute the layout early, improving rendering performance, especially when many columns are present.
Beyond the basic implementation, the article explores an advanced visual cue: dynamic shadows that appear only when the table content overflows. This is achieved with CSS gradients and the background‑attachment property (using the newer local keyword) to create a fixed‑position shadow that disappears as the user scrolls.
background: radial-gradient(at top, rgba(0,0,0,.2), transparent 70%) no-repeat;
background-size: 100% 15px;
/* shadow for the left side */
&.scroll-left .fixed-right {
border-bottom: 0.1px solid transparent !important;
.fixed-item {
width: 100%;
height: 100%;
box-shadow: 1px 57px 22px 0 rgba(0,0,0,0.2);
}
}
/* shadow for the right side */
&.scroll-right .fixed-left {
border-bottom: 0.1px solid transparent !important;
.fixed-item {
width: 100%;
height: 100%;
box-shadow: -1px 57px 22px 0 rgba(0,0,0,0.2);
}
}Finally, a usage example shows how to embed the custom component ( <biaoge> ) and provide a slot for row‑level actions such as toggling edit mode.
<biaoge :columns="columns" :data="data">
<template v-slot:default="{ row }">
<a-button style="height:22px;line-height:22px;" type="link" @click="toggleEdit(row)">
{{ row.isEdit ? '完成' : '修改' }}
</a-button>
</template>
</biaoge>The complete solution combines Vue’s reactivity, Element UI’s table component, CSS sticky positioning, and gradient‑based shadows to deliver a flexible, performant, and visually informative dynamic table.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.