A modern webes alkalmazások gerincét gyakran az adatok megjelenítése és kezelése képezi. Ehhez elengedhetetlen egy hatékony és rugalmas adat-táblázat komponens. Bár számtalan kész megoldás létezik (mint például a Vuetify data table, PrimeVue DataTable, vagy különböző harmadik féltől származó könyvtárak), vannak esetek, amikor a meglévő keretrendszerek túl sok kompromisszumot követelnek, túlságosan nagydarabok, vagy egyszerűen nem illeszkednek a projekt egyedi igényeihez. Ilyenkor jöhet szóba a nulláról való felépítés, ami teljes kontrollt és optimalizálási lehetőségeket biztosít. Ez a cikk részletesen bemutatja, hogyan hozhatunk létre egy komplex, testreszabható adat-táblázat komponenst Vue.js-ben, a kezdetektől a fejlettebb funkciókig.
Miért érdemes a nulláról építkezni?
Kezdő fejlesztők gyakran gondolják, hogy mindenre létezik egy kész könyvtár, és valahol igazuk is van. Azonban egyedi, magas minőségű felhasználói élmény megteremtéséhez, specifikus üzleti logikák implementálásához, vagy a csomagméret minimalizálásához elengedhetetlenné válhat a saját megoldás. A nulláról való építkezés nem csak mélyebb megértést ad a keretrendszer működéséről, hanem lehetővé teszi a teljes szabadságot a tervezésben és a fejlesztésben. Nincs többé szükség a felesleges funkciók kikapcsolására, vagy a könyvtár korlátainak áthidalására – mindent pontosan úgy alakíthatunk ki, ahogyan arra szükségünk van.
Alapvető követelmények egy „komplex” táblázattal szemben
Mielőtt belevágunk a kódolásba, tisztázzuk, mitől válik egy egyszerű HTML táblázat „komplex” adat-táblázat komponenssé. A legfontosabb funkciók a következők:
- Pagináció (Oldalazás): Nagy adathalmazok kezelése, oldalak közötti navigáció.
- Rendezés (Sorting): Oszlopok szerinti növekvő vagy csökkenő sorrendbe rendezés.
- Szűrés (Filtering/Searching): Globális keresés vagy oszlop-specifikus szűrők.
- Oszlopok Testreszabása: Oszlopok megjelenítése/elrejtése.
- Sorválasztás (Row Selection): Egyedi vagy több sor kijelölése.
- Betöltési állapotok: Vizuális visszajelzés az adatlekérdezés alatt.
- Reszponzivitás: Alkalmazkodás különböző képernyőméretekhez.
- Akadálymentesítés (Accessibility): A WCAG irányelvek betartása a felhasználói élmény javítása érdekében.
- Performancia: Hatékony működés nagy adathalmazok esetén is.
A Projekt felépítése és az alapok Vue.js-ben
Kezdjük egy új Vue.js projekt felállításával, például a Vue CLI vagy Vite segítségével:
vue create my-data-table-app
cd my-data-table-app
npm run serve
Vagy Vitével:
npm init vue@latest
cd my-data-table-app
npm install
npm run dev
Hozzuk létre a `components` mappában a `DataTable.vue` fájlt. Ez lesz a mi adat-táblázat komponensünk.
Az alapvető Vue komponens struktúra a következőképpen néz ki:
<template>
<div class="data-table-container">
<!-- Itt lesz a táblázat és a vezérlők -->
</div>
</template>
<script>
export default {
name: 'DataTable',
props: {
// Ide jönnek a bemeneti adatok
},
data() {
return {
// Itt tároljuk a belső állapotot
};
},
computed: {
// Származtatott tulajdonságok
},
methods: {
// Komponens logikája
}
};
</script>
<style scoped>
/* Lokális stílusok */
</style>
Adatkezelés és Props Definíció
A komponensnek szüksége lesz adatokra és arra, hogy tudja, mely oszlopokat kell megjelenítenie. Ehhez definiálunk két alapvető `prop`-ot:
- `items`: A megjelenítendő adatok tömbje (pl. `[{ id: 1, name: ‘Péter’, age: 30 }, …]`).
- `columns`: Az oszlopok definíciója, amely tartalmazza az oszlop címkéjét, a kulcsát (az `items` objektumban lévő property neve), és további beállításokat (pl. rendezhető-e).
// DataTable.vue script rész
props: {
items: {
type: Array,
required: true
},
columns: {
type: Array,
required: true,
// Példa oszlop definícióra:
// [{ key: 'id', label: 'ID', sortable: true, filterable: false }, ...]
}
},
data() {
return {
currentPage: 1,
pageSize: 10,
sortColumn: null,
sortDirection: 'asc', // 'asc' vagy 'desc'
searchTerm: ''
};
},
Lépésről lépésre: A Komplex Funkciók Implementálása
1. Az Alap Táblázat Struktúra
Először is, hozzuk létre az alap HTML táblázatot a `v-for` direktívák segítségével.
<template>
<div class="data-table-container">
<div class="controls">
<input type="text" v-model="searchTerm" placeholder="Keresés..." />
</div>
<table class="data-table">
<thead>
<tr>
<th v-for="col in columns" :key="col.key"
@click="col.sortable ? sortData(col.key) : null"
:class="{ 'sortable': col.sortable, 'sorted-asc': sortColumn === col.key && sortDirection === 'asc', 'sorted-desc': sortColumn === col.key && sortDirection === 'desc' }">
{{ col.label }}
<span v-if="col.sortable" class="sort-icon">
<span v-if="sortColumn === col.key">{{ sortDirection === 'asc' ? ' ↑' : ' ↓' }}</span>
<span v-else> ◇</span>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="item in paginatedItems" :key="item.id">
<td v-for="col in columns" :key="col.key + item.id">
{{ item[col.key] }}
</td>
</tr>
<tr v-if="paginatedItems.length === 0">
<td :colspan="columns.length" class="no-data">
Nincs megjeleníthető adat.
</td>
</tr>
</tbody>
</table>
<div class="pagination">
<!-- Pagináció vezérlők -->
</div>
</div>
</template>
2. Szűrés (Filtering)
A globális keresőmező már bekerült az előző kódba (`v-model=”searchTerm”`). Most valósítsuk meg a szűrési logikát egy `computed` property-ben.
// DataTable.vue script rész - computed
computed: {
filteredItems() {
if (!this.searchTerm) {
return this.items;
}
const lowerCaseSearchTerm = this.searchTerm.toLowerCase();
return this.items.filter(item =>
this.columns.some(col => {
// Csak a szűrhető oszlopokat vesszük figyelembe, ha van ilyen beállítás
// Ha nincs explicit 'filterable' flag, feltételezzük, hogy minden string oszlop szűrhető
const value = item[col.key];
return value && String(value).toLowerCase().includes(lowerCaseSearchTerm);
})
);
},
// ... ide jön majd a sortedItems és paginatedItems
}
3. Rendezés (Sorting)
A rendezési logika is `computed` property-ben kap helyet, és a `filteredItems`-re épül. A `sortData` metódus pedig frissíti a rendezési állapotot.
// DataTable.vue script rész - computed
computed: {
// ... filteredItems
sortedItems() {
if (!this.sortColumn) {
return this.filteredItems;
}
return [...this.filteredItems].sort((a, b) => {
const aValue = a[this.sortColumn];
const bValue = b[this.sortColumn];
if (aValue === bValue) return 0;
let comparison = 0;
if (typeof aValue === 'string' && typeof bValue === 'string') {
comparison = aValue.localeCompare(bValue);
} else {
comparison = aValue > bValue ? 1 : -1;
}
return this.sortDirection === 'asc' ? comparison : -comparison;
});
},
// ... ide jön majd a paginatedItems
},
// DataTable.vue script rész - methods
methods: {
sortData(columnKey) {
if (this.sortColumn === columnKey) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = columnKey;
this.sortDirection = 'asc';
}
this.currentPage = 1; // Rendezés után vissza az első oldalra
},
}
4. Pagináció (Pagination)
A pagináció szintén a `computed` property-k erejét használja ki, és a `sortedItems`-re épül.
// DataTable.vue script rész - computed
computed: {
// ... filteredItems, sortedItems
paginatedItems() {
const start = (this.currentPage - 1) * this.pageSize;
const end = start + this.pageSize;
return this.sortedItems.slice(start, end);
},
totalPages() {
return Math.ceil(this.sortedItems.length / this.pageSize);
},
totalItems() {
return this.sortedItems.length;
}
},
// DataTable.vue script rész - methods
methods: {
// ... sortData
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page;
}
},
prevPage() {
this.goToPage(this.currentPage - 1);
},
nextPage() {
this.goToPage(this.currentPage + 1);
}
}
És a pagináció UI része a `template`-ben:
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">Előző</button>
<span>Oldal {{ currentPage }} / {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage === totalPages">Következő</button>
<select v-model.number="pageSize">
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
</select>
</div>
5. Oszlopok Testreszabása (Column Customization)
Ez egy fejlettebb funkció, amely lehetővé teszi a felhasználók számára, hogy kiválasszák, mely oszlopokat szeretnék látni. Ehhez szükségünk van egy belső állapotra a látható oszlopokhoz, és egy menüre, ahol ezeket módosítani lehet.
// DataTable.vue script rész - data
data() {
return {
// ...
visibleColumnsKeys: [], // Kezdetben üres, vagy az összes oszlopot tartalmazza
};
},
// DataTable.vue script rész - mounted() lifecycle hook
mounted() {
this.visibleColumnsKeys = this.columns.map(col => col.key);
},
// DataTable.vue script rész - computed
computed: {
// ...
displayColumns() {
return this.columns.filter(col => this.visibleColumnsKeys.includes(col.key));
}
},
A `template`-ben a `displayColumns`-t használjuk a `
<div class="column-toggle">
<label v-for="col in columns" :key="'toggle-' + col.key">
<input type="checkbox" :value="col.key" v-model="visibleColumnsKeys" />
{{ col.label }}
</label>
</div>
Ne felejtsük el a `
6. Sorválasztás (Row Selection)
Egy checkbox oszlopot adunk hozzá a táblázathoz, valamint egy „összes kijelölése” checkboxot a fejlécbe. Az eredményt egy `selectedItems` tömbben tároljuk, és `emit` eseménnyel kommunikáljuk a szülő komponens felé.
// DataTable.vue script rész - data
data() {
return {
// ...
selectedItems: new Set(),
};
},
// DataTable.vue script rész - computed
computed: {
// ...
isAllSelected() {
if (this.paginatedItems.length === 0) return false;
return this.paginatedItems.every(item => this.selectedItems.has(item.id));
},
// A teljes adathalmaz összes elemének ID-ját feltételezzük, hogy van ID property
hasSelectedItems() {
return this.selectedItems.size > 0;
}
},
// DataTable.vue script rész - methods
methods: {
// ...
toggleSelectItem(item) {
if (this.selectedItems.has(item.id)) {
this.selectedItems.delete(item.id);
} else {
this.selectedItems.add(item.id);
}
this.$emit('selection-changed', Array.from(this.selectedItems));
},
toggleSelectAll() {
if (this.isAllSelected) {
this.paginatedItems.forEach(item => this.selectedItems.delete(item.id));
} else {
this.paginatedItems.forEach(item => this.selectedItems.add(item.id));
}
this.$emit('selection-changed', Array.from(this.selectedItems));
}
}
A `template`-ben hozzáadjuk a checkboxokat:
<!-- A thead-ben -->
<th>
<input type="checkbox" :checked="isAllSelected" @change="toggleSelectAll" />
</th>
<!-- A tbody-ban -->
<td>
<input type="checkbox" :checked="selectedItems.has(item.id)" @change="toggleSelectItem(item)" />
</td>
7. Betöltési állapotok és üres táblázat
Egy `isLoading` prop segítségével jelezzük a felhasználónak, hogy adatok betöltése folyik. Ezt a szülő komponens felelőssége kezelni (pl. API hívás alatt). Hozzáadunk egy üres állapotot is.
// DataTable.vue script rész - props
props: {
// ...
isLoading: {
type: Boolean,
default: false
}
},
A `template`-ben:
<tbody>
<tr v-if="isLoading">
<td :colspan="displayColumns.length + (showSelection ? 1 : 0)" class="loading-message">
Adatok betöltése...
</td>
</tr>
<tr v-else-if="paginatedItems.length === 0">
<td :colspan="displayColumns.length + (showSelection ? 1 : 0)" class="no-data">
Nincs megjeleníthető adat.
</td>
</tr>
<tr v-else v-for="item in paginatedItems" :key="item.id">
<!-- ... cellák -->
</tr>
</tbody>
8. Reszponzivitás (Responsiveness)
A táblázatok reszponzivitása gyakran kihívás. A legegyszerűbb megoldás az `overflow-x: auto` használata a táblázatot tartalmazó div-en, így a felhasználó oldalra görgethet, ha a tartalom túl széles.
<div class="table-wrapper">
<table class="data-table">
<!-- ... -->
</table>
</div>
<style scoped>
.table-wrapper {
overflow-x: auto;
}
.data-table {
width: 100%; /* Fontos, hogy a táblázat legalább 100% széles legyen */
border-collapse: collapse;
}
/* ... további stílusok ... */
</style>
Fejlettebb reszponzivitás elérése érdekében mobil nézetben kártyákra bonthatjuk az adatokat, vagy prioritási alapon rejthetünk el oszlopokat.
9. Akadálymentesítés (Accessibility – ARIA)
Az akadálymentes táblázatok kulcsfontosságúak. Használjunk megfelelő ARIA attribútumokat:
- `scope=”col”` a `
` elemeken. - `aria-sort=”ascending”` vagy `descending”` a rendezhető oszlopfejléceken.
- `aria-label` a gombokon (pl. pagináció).
<th v-for="col in displayColumns" :key="col.key" scope="col" :aria-sort="sortColumn === col.key ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'" @click="col.sortable ? sortData(col.key) : null"> {{ col.label }} </th> <button @click="prevPage" :disabled="currentPage === 1" aria-label="Előző oldal">Előző</button>
Server-Side vs. Client-Side Feldolgozás
Eddig feltételeztük, hogy minden adat a kliensoldalon van, és ott is dolgozzuk fel. Ez kisebb és közepes adathalmazok esetén (néhány ezer sor) teljesen megfelelő. Azonban nagy, vagy nagyon nagy adathalmazok (tízezrek, százezrek, milliók) esetén ez a megközelítés súlyos performancia problémákat okozhat. Itt jön képbe a szerveroldali feldolgozás.
Szerveroldali feldolgozás esetén a táblázat komponens csak a megjelenítésért felel. A szűrés, rendezés és pagináció logikája a backend API-n fut. A Vue komponens ilyenkor csupán eseményeket bocsát ki (`emit`), jelezve a szülő komponensnek, hogy a felhasználó például rendezni szeretne egy oszlopot, vagy lapot váltott. A szülő komponens ezekre az eseményekre reagálva hívja meg az API-t a megfelelő paraméterekkel (pl. `api/data?page=2&sort_by=name&sort_dir=asc&search=value`), és az API a szűrt, rendezett, lapozott adatokat küldi vissza. A táblázat ezután egyszerűen megjeleníti a friss adatokat. Ez a megközelítés sokkal skálázhatóbb és hatékonyabb.
Performancia Optimalizálás
- Debounce a keresőmezőn: Ne indítsunk szűrést minden egyes billentyűleütésnél. Várjunk egy rövid ideig (pl. 300 ms), mielőtt elindítjuk a szűrési logikát. Ez csökkenti a felesleges számításokat.
- `v-for` `key` attribútum: Mindig használjunk `:key` attribútumot a `v-for` listákban, hogy a Vue hatékonyan tudja frissíteni a DOM-ot.
- Számított tulajdonságok láncolása: Ahogy láttuk (`items` -> `filteredItems` -> `sortedItems` -> `paginatedItems`), a számított tulajdonságok láncolása optimalizálja a Vue reaktivitását.
- Virtuális görgetés (Virtual Scrolling): Nagyon nagy, de kliensoldalon kezelt adathalmazoknál a virtuális görgetés technikája (csak a látható sorok renderelése) drámaian javíthatja a performanciát. Ez azonban egy komplexebb feature, amit érdemes különálló könyvtárra bízni, ha szükség van rá.
További Fejlesztési Lehetőségek
Miután az alapok stabilan működnek, számos módon bővíthetjük a komponenst:
- Oszlopok átrendezése (Drag & Drop): Húzd és ejtsd funkcióval a felhasználók átrendezhetik az oszlopokat.
- Egyedi cella tartalom (Slots): Hagyjuk, hogy a szülő komponens egyedi tartalmat rendereljen a táblázat celláiban (pl. akciógombok, ikonok).
- Inline szerkesztés: A cellák tartalmának közvetlen szerkesztése a táblázatban.
- Adat exportálás: CSV, Excel vagy PDF exportálás lehetősége.
- Felhasználói beállítások mentése: Mentse el a felhasználó által beállított oszlopláthatóságot, rendezési preferenciákat a Local Storage-ben.
Összefoglalás
Egy komplex adat-táblázat komponens felépítése a nulláról Vue.js-ben egy jelentős feladat, de a befektetett energia megtérül a rugalmasság és a teljesítmény terén. A kezdeti lépésektől (adatkezelés, pagináció, szűrés, rendezés) a fejlettebb funkciókig (oszlop testreszabás, sorválasztás, reszponzivitás, akadálymentesítés), minden lépés a Vue.js reaktív rendszerére épül, biztosítva a hatékonyságot és az eleganciát.
Ez a megközelítés nem csak mélyebb megértést nyújt a keretrendszerről, hanem lehetővé teszi, hogy pontosan olyan megoldást hozzunk létre, amilyenre szükségünk van, felesleges függőségek nélkül. A moduláris felépítésnek köszönhetően a komponens könnyen karbantartható és bővíthető marad, készen állva a jövőbeli kihívásokra. Ne féljünk tehát belevágni, ha a kész megoldások nem elegendőek – a saját komponens építése izgalmas és rendkívül tanulságos utazás a front-end fejlesztés világában!
Leave a Reply