ramondev / html-table-builder
Builds and renders an HTML table from an array or Eloquent collection
Package info
github.com/ramondev180/laravel-html-table-builder
pkg:composer/ramondev/html-table-builder
Requires
- php: ^8.2
- illuminate/database: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/view: ^9.0|^10.0|^11.0|^12.0|^13.0
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, andremoveColumnsrebuild the internal row data immediately — call them beforecolumnFormat,setTableRowAttr, etc.columnFormatandeachindex 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/setTableDataAttrrefer 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.