Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Created September 4, 2026 09:32
Show Gist options
  • Select an option

  • Save atomjoy/ece5234b9d2f398b202cf278a44b1b5a to your computer and use it in GitHub Desktop.

Select an option

Save atomjoy/ece5234b9d2f398b202cf278a44b1b5a to your computer and use it in GitHub Desktop.
Laravel Inertia table.

Laravel Inertia Table

Backend

<?php

namespace App\Http\Controllers;

use App\Models\Todo;
use Illuminate\Http\Request;
use Inertia\Inertia;

class TodoController extends Controller
{
    public function index(Request $request)
    {
        // Rozpoczynamy budowanie zapytania Eloquent
        $query = Todo::query();

        // 1. SZUKANIE (Server-side)
        if ($request->filled('search')) {
            $query->where('todo', 'like', '%' . $request->search . '%');
        }

        // 2. FILTROWANIE PO STATUSIE (Server-side)
        if ($request->filled('status') && $request->status !== 'all') {
            $isCompleted = $request->status === 'completed';
            $query->where('completed', $isCompleted);
        }

        // 3. SORTOWANIE (Zabezpieczone białą listą kolumn)
        $sortBy = $request->input('sortBy', 'id');
        $order = $request->input('order', 'asc');

        $allowedSorts = ['id', 'todo', 'completed'];
        if (in_array($sortBy, $allowedSorts) && in_array($order, ['asc', 'desc'])) {
            $query->orderBy($sortBy, $order);
        }

        // 4. PAGINACJA (Laravel automatycznie dołączy parametry query string do linków)
        $limit = $request->input('limit', 10);
        $todos = $query->paginate($limit)->withQueryString();

        // Wysyłamy dane do Vue przez Inertia razem z aktualnymi filtrami
        return Inertia::render('Todos/Index', [
            'todos' => $todos,
            'filters' => $request->only(['search', 'status', 'sortBy', 'order', 'limit'])
        ]);
    }
}

Frontend

<script setup>
import { ref, watch } from 'vue'
import { router } from '@inertiajs/vue3'

// Przechwytujemy dane przesłane z Laravela jako props
const props = defineProps({
  todos: Object, // Zawiera dane o wierszach oraz strukturę paginacji (links, total itp.)
  filters: Object, // Pamięta, jakie filtry były aktywne, żeby formularze się nie resetowały
})

// JEDEN obiekt stanu aplikacji, zsynchronizowany z tym, co przysłał serwer
const tableState = ref({
  search: props.filters.search || '',
  status: props.filters.status || 'all',
  sortBy: props.filters.sortBy || 'id',
  order: props.filters.order || 'asc',
  limit: props.filters.limit || 10,
})

// Funkcja, która wysyła aktualny stan tabeli do Laravela
function updateTable() {
  router.get(
    '/todos',
    { ...tableState.value }, // Przekazujemy cały obiekt filtrów jako query params (?page=2&search=...)
    {
      preserveState: true, // Zapobiega resetowaniu stanu komponentów Vue (np. focus w input)
      preserveScroll: true, // Zapobiega skakaniu strony do góry po przeładowaniu danych
      replace: true, // Zamiast tworzyć nową historię w przeglądarce przy każdej literce, nadpisuje obecną
    },
  )
}

// Reagujemy na zmianę statusu lub limitu stron – natychmiast odświeżamy tabelę
watch(
  () => [tableState.value.status, tableState.value.limit],
  () => {
    updateTable()
  },
)

// Obsługa wyszukiwania (tutaj przydałby się prosty debounce, żeby nie strzelać serwerem co literkę)
function onSearch() {
  updateTable()
}

// Funkcja zmieniająca sortowanie kolumny
function handleSort(columnKey) {
  if (tableState.value.sortBy === columnKey) {
    tableState.value.order = tableState.value.order === 'asc' ? 'desc' : 'asc'
  } else {
    tableState.value.sortBy = columnKey
    tableState.value.order = 'asc'
  }
  updateTable()
}
</script>

<template>
  <div class="table-container">
    <h2>Zadania – Laravel 13 + Inertia (Czysty JS)</h2>

    <!-- PANEL FILTRÓW -->
    <div class="filters-panel">
      <input
        v-model="tableState.search"
        @input="onSearch"
        type="text"
        placeholder="Wyszukaj zadanie..."
        class="search-input"
      />

      <select v-model="tableState.status" class="status-select">
        <option value="all">Wszystkie statusy</option>
        <option value="completed">Tylko ukończone ✅</option>
        <option value="pending">W trakcie ⏳</option>
      </select>
    </div>

    <!-- TABELA HTML -->
    <table>
      <thead>
        <tr>
          <th @click="handleSort('id')" class="sortable">
            ID
            <span v-if="tableState.sortBy === 'id'">{{
              tableState.order === 'asc' ? '🔼' : '🔽'
            }}</span>
          </th>
          <th @click="handleSort('todo')" class="sortable">
            Zadanie
            <span v-if="tableState.sortBy === 'todo'">{{
              tableState.order === 'asc' ? '🔼' : '🔽'
            }}</span>
          </th>
          <th @click="handleSort('completed')" class="sortable">
            Status
            <span v-if="tableState.sortBy === 'completed'">{{
              tableState.order === 'asc' ? '🔼' : '🔽'
            }}</span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-if="todos.data.length === 0">
          <td
            colspan="3"
            style="text-align: center; color: #888; padding: 20px;"
          >
            Brak wyników spełniających kryteria.
          </td>
        </tr>
        <!-- Iterujemy po danych bezpośrednio z paginatora Laravela -->
        <tr v-for="todo in todos.data" :key="todo.id">
          <td>{{ todo.id }}</td>
          <td>{{ todo.todo }}</td>
          <td>{{ todo.completed ? '✅ Ukończone' : '⏳ W trakcie' }}</td>
        </tr>
      </tbody>
    </table>

    <!-- PAGINACJA OPARTA NA LINKACH Z LARAVELA -->
    <div class="pagination-panel">
      <!-- Przycisk Poprzednia -->
      <button
        :disabled="!todos.prev_page_url"
        @click="
          router.get(
            todos.prev_page_url,
            {},
            { preserveScroll: true, preserveState: true },
          )
        "
      >
        Poprzednia
      </button>

      <span>Strona {{ todos.current_page }} z {{ todos.last_page }}</span>

      <!-- Przycisk Następna -->
      <button
        :disabled="!todos.next_page_url"
        @click="
          router.get(
            todos.next_page_url,
            {},
            { preserveScroll: true, preserveState: true },
          )
        "
      >
        Następna
      </button>

      <!-- Wybór limitu wierszy -->
      <select v-model="tableState.limit" class="limit-select">
        <option :value="5">Pokazuj: 5</option>
        <option :value="10">Pokazuj: 10</option>
        <option :value="20">Pokazuj: 20</option>
      </select>
    </div>
  </div>
</template>

<style scoped>
.table-container {
  font-family: sans-serif;
  margin: 20px;
  max-width: 900px;
}
.filters-panel {
  display: flex;
  gap: 15px;
  margin-bottom: 20px;
}
.search-input {
  flex-grow: 1;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}
.status-select,
.limit-select {
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  cursor: pointer;
}
table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 20px;
}
th,
td {
  border: 1px solid #ddd;
  padding: 12px;
  text-align: left;
}
th {
  background-color: #f4f4f4;
}
.sortable {
  cursor: pointer;
  user-select: none;
}
.sortable:hover {
  background-color: #e9e9e9;
}
tr:nth-child(even) {
  background-color: #f9f9f9;
}
.pagination-panel {
  display: flex;
  gap: 15px;
  align-items: center;
}
.pagination-panel button {
  padding: 8px 12px;
  cursor: pointer;
}
.pagination-panel button:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment