⬡ Lacelium
⌘K
Version 2.x ▾
Reactive UI

DataTable Pattern

Lacelium champions a "Server-Side HTML First" architecture for reactive data tables, combining robust database filtering, multi-column sorting, and pagination with seamless asynchronous SPA transitions.

HTML-First Reactive Architecture

Instead of heavy client-side JavaScript tables (which download megabytes of raw JSON, risk memory leaks, and break search engine indexing), Lacelium performs querying, slicing, and rendering server-side. Sorting and filtering occur via standard query parameters (?sort=-priority,created_at&status=active) swapped into the DOM seamlessly via AJAX.

Interactive Sandbox Table

Click table column headers to test interactive sorting with visual ascending and descending indicators:

ID ↕ Task Name ↕ Status ↕
#1024 Async Job Pipeline Active
#1025 Redis Session Cache Pending
#1026 ArgusKit Studio Sync Syncing
#1027 Database Read-Replica Active

Controller Implementation

Controllers parse incoming sorting keys and apply them to the query builder before passing the builder to the paginator:

namespace App\Http\Controllers;

use App\Models\Task;
use Lacelium\Core\Http\Request;
use Lacelium\Core\Http\Response;
use Lacelium\Core\Pagination\Paginator;

class TaskTableController
{
    public function index(Request $request, Response $response): Response
    {
        $query = Task::query()->where('is_active', true);

        // 1. Multi-Column Sorting: ?sort=-priority,title
        if ($sort = $request->query('sort')) {
            foreach (explode(',', $sort) as $column) {
                $direction = str_starts_with($column, '-') ? 'desc' : 'asc';
                $field     = ltrim($column, '-');
                $query->orderBy($field, $direction);
            }
        } else {
            $query->orderByDesc('id');
        }

        // 2. Query Filtering
        if ($status = $request->query('status')) {
            $query->where('status', $status);
        }

        // 3. Paginate
        $paginator = Paginator::fromQuery($query, $request, perPage: 15);
        $tasks     = (clone $query)->forPage($paginator->currentPage, 15)->get();

        return $response->render('docs/tasks/table.twig', [
            'tasks'     => $tasks,
            'paginator' => $paginator,
        ]);
    }
}

Twig Template Integration

Use data attributes on column header links. Lacelium automatically binds click handlers to update the URL and trigger SPA fragment swaps:


        {% for task in tasks %}
            
        {% endfor %}
    
Title {% if current_sort == 'title' %}↑{% elseif current_sort == '-title' %}↓{% endif %} Priority {% if current_sort == 'priority' %}↑{% elseif current_sort == '-priority' %}↓{% endif %}
{{ task.title }} {{ task.priority }}
{% include 'docs/paginator.twig' with { paginator: paginator } %}

Multi-Column Shift + Click Interaction

Lacelium's table script natively supports composite sorting keys:

User Action Query String Result Behavior
Standard Click ?sort=priority Clears previous sort rules and sorts exclusively by the clicked column.
Shift + Click ?sort=priority,-title Appends or toggles the column in the multi-column composite sorting chain.

Performance Best Practices

  • Compound SQL Indexes: When supporting multi-column sorting (e.g. status + created_at), create corresponding compound database indexes.
  • Allowed Sort Whitelisting: Always validate sort column names against an allowed whitelist in your controller to prevent SQL injection or arbitrary column disclosure.
  • Query Cloning: Always execute clone $query before calling ->forPage() to keep the paginator instance unmutated.

Next Steps