ramondev/html-table-builder

Builds and renders an HTML table from an array or Eloquent collection

Maintainers

Package info

github.com/ramondev180/laravel-html-table-builder

pkg:composer/ramondev/html-table-builder

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-12 11:34 UTC

This package is auto-updated.

Last update: 2026-08-12 11:43:07 UTC


README

Build and render HTML tables from an array or Eloquent collection, with dynamic columns, per-cell formatting, custom HTML attributes, output escaping, pagination, and an empty state — all from plain PHP, no Blade component boilerplate required in your own app.

Requirements

  • PHP ^8.0.2
  • Laravel 9 – 13 (illuminate/support, illuminate/database, illuminate/view)

Installation

composer require ramondev/html-table-builder

The service provider (Ramondev\HtmlTableBuilder\HTMLTableBuilderProvider) auto-registers via Laravel's package discovery — no manual step needed. It registers the package's Blade view under the views:: namespace, so the view file ships with the package rather than living in your app's resources/views.

Quick start

use Ramondev\HtmlTableBuilder\HtmlTableBuilder;

$table = new HtmlTableBuilder($users); // array or Eloquent Collection

return $table->render();

By default this renders every key found across the rows as a column, with values HTML-escaped.

// In a controller
public function index()
{
    $table = new HtmlTableBuilder(User::all());

    return view('users.index', ['table' => $table]);
}
{{-- users/index.blade.php --}}
<div class="users-table">
    {!! $table->render() !!}
</div>

Constructing a table

new HtmlTableBuilder(array|object $data)

$data can be:

  • A plain array of associative arrays (one per row), e.g. [['id' => 1, 'name' => 'Ada'], ...]
  • An Illuminate\Database\Eloquent\Collection — converted to an array automatically

Columns are auto-detected as the union of all keys found across every row.

Columns

Each column is either:

  • A plain string key: 'name' — used as both the data key and the header label
  • A [key, label] pair: ['name', 'Full Name'] — key looks up the value, label is shown in the header

setColumns(array $columns): void

Replace the column list entirely.

$table->setColumns(['id', ['name', 'Full Name'], 'email']);

appendColumn(array $columns): void

Add columns to the end of the existing list.

$table->appendColumn([['actions', 'Actions']]);

removeColumns(array $columns): void

Remove columns by key.

$table->removeColumns(['password', 'remember_token']);

tableHeadFormat(callable $callback): void

Transform every column's label (not its key) through a callback.

$table->tableHeadFormat(fn (string $label) => strtoupper($label));

Cell values

columnFormat(string $key, callable $callback): void

Override the rendered value of a column for every row. The callback receives the original row (as an object) and its index.

$table->columnFormat('created_at', fn ($row, $i) => $row->created_at->format('M j, Y'));

$table->columnFormat('status', fn ($row) => $row->is_active ? 'Active' : 'Inactive');

If the callback returns HTML (e.g. a badge <span>), pair it with escapeOutput(false, 'status') below — otherwise the markup renders as literal text.

each(callable $callback): void

Iterate the raw source data, with $this (the table) passed in so the callback can configure things per row.

$table->each(function (HtmlTableBuilder $table, int $index, array $row) {
    if ($row['is_flagged']) {
        $table->setTableRowAttr(['class' => 'bg-red-50'], $index);
    }
});

Output escaping

Values are HTML-escaped by default.

escapeOutput(bool $escape, string|array|null $key = null): void

$table->escapeOutput(false);                          // disable escaping for ALL columns
$table->escapeOutput(false, 'status');                // disable escaping for just the "status" column
$table->escapeOutput(false, ['status', 'actions']);   // disable escaping for several columns in one call

Calling with no $key sets the global default and clears any earlier per-column overrides. Calling with a key (or array of keys) only touches those columns.

Only disable escaping for values you control or have already sanitized — unescaped input renders as raw HTML/JS.

HTML attributes

Attribute values passed to any of these setters are automatically escaped.

$table->setTableAttr(['class' => 'table table-striped', 'id' => 'users-table']);
$table->setTableHeadAttr(['class' => 'bg-gray-100']);
$table->setTableBodyAttr(['class' => 'divide-y']);

$table->setTableRowAttr(['class' => 'border-b']);          // every row
$table->setTableRowAttr(['class' => 'bg-yellow-50'], 3);   // just row index 3

$table->setTableDataAttr(['class' => 'px-4 py-2']);             // every cell
$table->setTableDataAttr(['class' => 'font-bold'], 0, 'name');  // row 0, "name" column only

Pagination

paginate(int $perPage, ?int $page = null): void

Enable pagination. If $page is omitted, the current page is read automatically from the request's query string (?page=2 by default).

$table->paginate(15);

setPaginationPageName(string $name): void

Change the query string key — useful when a page has more than one paginated table.

$table->setPaginationPageName('users_page'); // links become ?users_page=2

setPaginationLabels(string $previous, string $next): void

$table->setPaginationLabels('← Prev', 'Next →');

setPaginationWindow(int $onEachSide): void

How many page numbers show on each side of the current page before collapsing into .... Default is 3, e.g. 1 ... 4 5 [6] 7 8 ... 12. Only visible once there are enough total pages to need collapsing.

$table->setPaginationWindow(1); // tighter: 1 ... 5 [6] 7 ... 12

setPaginationNavAttr() / setPaginationListAttr() / setPaginationLinkAttr(array $attrs): void

HTML attributes for the <nav> wrapper, the <ul>, and every <a> link.

$table->setPaginationNavAttr(['class' => 'mt-4']);
$table->setPaginationListAttr(['class' => 'flex gap-2']);
$table->setPaginationLinkAttr(['class' => 'pagination-link']);

hidePaginationControls(): void

Rows are still sliced to the current page, but the built-in Previous/Next/ number UI isn't rendered.

$table->hidePaginationControls();

getPaginationMeta(): array

Returns the pagination state so you can build a fully custom UI, typically combined with hidePaginationControls().

$meta = $table->getPaginationMeta();
// ['enabled' => true, 'currentPage' => 2, 'lastPage' => 5, 'total' => 68, 'pages' => [1, '...', 2, 3, 4, '...', 5], ...]

Pagination reads/builds the query string via request(), so it requires an active HTTP request — not intended for CLI/artisan contexts.

Empty state

emptyState(string $message, array $attrs = [], bool $escape = true): void

Configure the row shown when there is no data (or the current page is empty).

$table->emptyState('No users found.');

$table->emptyState('No users found.', ['class' => 'empty-row']);

$table->emptyState('<strong>No users</strong> match your filters.', [], escape: false);

Full example

use Ramondev\HtmlTableBuilder\HtmlTableBuilder;

$table = new HtmlTableBuilder($users);

$table->setColumns(['id', ['name', 'Full Name'], 'email', ['status', 'Status']]);

$table->columnFormat('status', fn ($row) =>
    $row->is_active
        ? '<span class="badge badge-success">Active</span>'
        : '<span class="badge badge-muted">Inactive</span>'
);
$table->escapeOutput(false, 'status');

$table->setTableAttr(['class' => 'table table-striped w-full']);
$table->setTableHeadAttr(['class' => 'bg-gray-100 text-left']);
$table->setTableRowAttr(['class' => 'border-b hover:bg-gray-50']);

$table->paginate(20);
$table->setPaginationLabels('← Previous', 'Next →');
$table->setPaginationWindow(2);

$table->emptyState('No users match your filters.', ['class' => 'py-8 text-center']);

return $table->render();

Notes / gotchas

  • setColumns, appendColumn, and removeColumns rebuild the internal row data immediately — call them before columnFormat, setTableRowAttr, etc.
  • columnFormat and each index into the source data by row index — keep row indices stable (avoid re-keying the data before passing it in).
  • Row/cell attribute keys for setTableRowAttr/setTableDataAttr refer to the original row index from the source data, not the position within the current pagination page.
  • With few total pages, setPaginationWindow() won't visibly change anything — the page list only collapses once there are more pages than the window's threshold allows.

License

MIT — see LICENSE.