Laravel 13 Inertia, Vue i zapytania z useQuery do api.
npm i @tanstack/vue-table@9
npm i @tanstack/vue-query@5/api/users
<?php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// ->middleware('auth:sanctum');
// Route: /api/users
Route::get('/users', function (Request $request) {
$request->validate([
'sort' => 'sometimes|string',
'direction' => 'sometimes|string|in:asc,desc',
'per_page' => 'sometimes|integer|min:1|max:100',
]);
$query = User::query();
// 2. Sortowanie (identycznie jak wcześniej)
if ($request->has('sort')) {
$sortBy = $request->input('sort');
$direction = $request->input('direction', 'asc');
$allowedColumns = ['id', 'name', 'email', 'role'];
if (in_array($sortBy, $allowedColumns)) {
$query->orderBy($sortBy, $direction);
}
} else {
$query->latest('id');
}
$perPage = $request->input('per_page', 10);
$users = $query->paginate($perPage);
// Sorting example
// return response()->json(['items' => User::all()]); // sorting
// Zwraca pełny obiekt Laravel LengthAwarePaginator
return response()->json($users);
})->name('api.users');/users
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
class UserController extends Controller
{
public function index(Request $request): Response
{
$users = User::all();
// Change to user or items
return Inertia::render('admin/users/table/Index', [
'data' => $users,
// 'filters' => $request->only(['search', 'sort', 'direction', 'per_page', 'roles', 'start_date', 'end_date']),
// 'roleFacets' => $this->getRoleFacets($request),
]);
}
}admin/users/table/Index.vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useQuery } from '@tanstack/vue-query'
import {
useTable,
tableFeatures,
columnVisibilityFeature,
rowSortingFeature,
rowPaginationFeature,
createColumnHelper,
FlexRender,
type Updater,
type SortingState,
type PaginationState,
} from '@tanstack/vue-table'
type User = {
id: number
name: string
email: string
role: string
}
// Definicja typu odpowiedzi z Laravela
type LaravelPaginationResponse = {
data: User[]
current_page: number
last_page: number
per_page: number
total: number
}
// Get data
const fetchUsers = async (
currentSorting: SortingState,
currentPagination: PaginationState,
): Promise<LaravelPaginationResponse> => {
// TanStack Table indeksuje od 0, Laravel od 1, stąd "+ 1"
const page = currentPagination.pageIndex + 1
const perPage = currentPagination.pageSize
let url = `/api/users?page=${page}&per_page=${perPage}`
if (currentSorting.length > 0) {
const sortInfo = currentSorting[0]
const sortBy = sortInfo.id
const order = sortInfo.desc ? 'desc' : 'asc'
url += `&sort=${sortBy}&direction=${order}`
}
const res = await fetch(url)
if (!res.ok) throw new Error('Błąd pobierania użytkowników')
return res.json()
}
const sorting = ref<SortingState>([])
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 10 })
// Integracja z TanStack Query (Query Key obserwuje teraz stany)
const { data, isLoading, error } = useQuery({
queryKey: ['users', sorting, pagination],
queryFn: () => fetchUsers(sorting.value, pagination.value),
})
const tableData = computed(() => data.value?.data ?? [])
const pageCount = computed(() => data.value?.last_page ?? -1)
// Rejestrujemy funkcjonalności dla v9
const features = tableFeatures({
columnVisibilityFeature,
rowSortingFeature,
rowPaginationFeature,
})
const columnHelper = createColumnHelper<typeof features, User>()
const columns = columnHelper.columns([
columnHelper.accessor('id', { header: 'ID' }),
columnHelper.accessor('name', { header: 'Imię i nazwisko' }),
columnHelper.accessor('email', { header: 'Adres E-mail' }),
columnHelper.accessor('role', { header: 'Rola' }),
])
const table = useTable({
features,
get data() {
return tableData.value
},
columns,
manualSorting: true,
manualPagination: true,
pageCount: pageCount.value,
state: {
get sorting() {
return sorting.value
},
get pagination() {
return pagination.value
},
},
onSortingChange: (updater: Updater<SortingState>) => {
sorting.value = typeof updater === 'function' ? updater(sorting.value) : updater
pagination.value.pageIndex = 0
},
onPaginationChange: (updater: Updater<PaginationState>) => {
pagination.value = typeof updater === 'function' ? updater(pagination.value) : updater
},
})
</script>
<template>
<div style="padding: 20px">
<div v-if="isLoading">Ładowanie użytkowników...</div>
<div v-else-if="error">Błąd: {{ error.message }}</div>
<div v-else>
<table style="width: 100%; border-collapse: collapse; text-align: left">
<thead>
<tr
v-for="headerGroup in table.getHeaderGroups()"
:key="headerGroup.id"
style="border-bottom: 2px solid #ccc"
>
<th
v-for="header in headerGroup.headers"
:key="header.id"
style="padding: 10px; user-select: none"
>
<div
v-if="!header.isPlaceholder"
:style="
header.column.getCanSort()
? 'cursor: pointer; display: flex; gap: 8px;'
: ''
"
@click="header.column.getToggleSortingHandler()?.($event)"
>
<FlexRender
:render="header.column.columnDef.header"
:props="header.getContext()"
/>
<span v-if="header.column.getIsSorted() === 'asc'">🔼</span>
<span v-else-if="header.column.getIsSorted() === 'desc'">🔽</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="row in table.getRowModel().rows"
:key="row.id"
style="border-bottom: 1px solid #eee"
>
<td
v-for="cell in row.getVisibleCells()"
:key="cell.id"
style="padding: 10px"
>
<FlexRender
:render="cell.column.columnDef.cell"
:props="cell.getContext()"
/>
</td>
</tr>
</tbody>
</table>
<!-- Kontrolki paginacji dolnej zgodne z v9 (Vue) -->
<div
style="
margin-top: 15px;
display: flex;
align-items: center;
gap: 10px;
font-family: sans-serif;
"
>
<button
:disabled="pagination.pageIndex === 0"
@click="pagination.pageIndex--"
style="padding: 5px 10px; cursor: pointer"
>
Poprzednia
</button>
<span>
Strona <strong>{{ pagination.pageIndex + 1 }}</strong> z
<strong>{{ pageCount }}</strong>
</span>
<button
:disabled="pagination.pageIndex + 1 >= pageCount"
@click="pagination.pageIndex++"
style="padding: 5px 10px; cursor: pointer"
>
Następna
</button>
<select
:value="pagination.pageSize"
@change="
((pagination.pageSize = Number(($event.target as HTMLSelectElement).value)),
(pagination.pageIndex = 0))
"
style="padding: 5px"
>
<option v-for="size in [1, 5, 10, 20, 50]" :key="size" :value="size">
Pokaż {{ size }}
</option>
</select>
</div>
</div>
</div>
</template>