Egy komplex adat-táblázat komponens felépítése a nulláról Vue.js-ben

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 `

` és `

` elemek rendereléséhez, és egy legördülő menüvel vezéreljük a `visibleColumnsKeys` állapotot. Például:


<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 `

` és `

` ciklusokat `displayColumns`-ra cserélni!

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

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük