internetguru / laravel-model-browser
A Laravel package to browse models and show them in a table, autocmplete, etc.
Package info
github.com/internetguru/laravel-model-browser
pkg:composer/internetguru/laravel-model-browser
Requires
- php: ^8.4
- internetguru/laravel-common: ^7 || ^8
- laravel/framework: ^13.0
- livewire/livewire: ^4.0
Requires (Dev)
- internetguru/laravel-common: ^7 || ^8
- laravel/pint: ^1.17
- livewire/livewire: ^4.0
- orchestra/testbench: ^11.0
- phpunit/php-code-coverage: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v8.2.2
- v8.2.1
- v8.2.0
- v8.1.0
- v8.0.1
- v8.0.0
- v7.4.3
- v7.4.2
- v7.4.1
- v7.4.0
- v7.3.1
- v7.3.0
- v7.2.0
- v7.1.0
- v7.0.0
- v6.6.1
- v6.6.0
- v6.5.0
- v6.4.1
- v6.4.0
- v6.3.0
- v6.2.0
- v6.1.1
- v6.1.0
- v6.0.1
- v6.0.0
- v5.3.3
- v5.3.2
- v5.3.1
- v5.3.0
- v5.2.1
- v5.2.0
- v5.1.1
- v5.1.0
- v5.0.0
- v4.2.1
- v4.2.0
- v4.1.7
- v4.1.6
- v4.1.5
- v4.1.4
- v4.1.3
- v4.1.2
- v4.1.1
- v4.1.0
- v4.0.3
- v4.0.2
- v4.0.1
- v4.0.0
- v3.2.0
- v3.1.9
- v3.1.8
- v3.1.7
- v3.1.6
- v3.1.5
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.9
- v3.0.8
- v3.0.7
- v3.0.6
- v3.0.5
- v3.0.4
- v3.0.3
- v3.0.2
- v3.0.1
- v3.0.0
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.4.1
- v1.4.0
- v1.3.0
- v1.2.0
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- v0.12.2
- v0.12.1
- v0.12.0
- v0.11.0
- v0.10.0
- v0.9.0
- v0.8.6
- v0.8.5
- v0.8.4
- v0.8.3
- v0.8.2
- v0.8.1
- v0.8.0
- v0.7.0
- v0.6.5
- v0.6.4
- v0.6.3
- v0.6.2
- v0.6.1
- v0.6.0
- v0.5.5
- v0.5.4
- v0.5.3
- v0.5.2
- v0.5.1
- v0.5.0
- v0.4.8
- v0.4.7
- v0.4.6
- v0.4.5
- v0.4.4
- v0.4.3
- v0.4.2
- v0.4.1
- v0.4.0
- v0.3.0
- v0.2.3
- v0.2.2
- v0.2.1
- v0.2.0
- v0.1.0
- dev-dev
- dev-main-8
- dev-staging
- dev-main-7
- dev-main-6
- dev-main-5
- dev-main-4
- dev-main-3
- dev-main-2
- dev-main-1
- dev-main-0
This package is auto-updated.
Last update: 2026-09-28 09:28:40 UTC
README
A Laravel package to browse models and show them in cards, tables, etc.
| Branch | Status | Code Coverage |
|---|---|---|
| Main |
Requirements
- PHP 8.4+
- Laravel 11, 12, or 13
- Livewire 4
Livewire 3 → 4: As of version 4.3, this package requires Livewire 4. Support for Livewire 3 (and Laravel 9/10) has been dropped. The components are class-based and registered by the package's service provider, so no application changes are needed beyond upgrading Livewire itself.
Installation
-
Install the package via Composer:
# First time installation composer require internetguru/laravel-model-browser # For updating the package composer update internetguru/laravel-model-browser
-
Optionally publish the config, views, and translations:
php artisan vendor:publish --tag=config --provider="Internetguru\ModelBrowser\ModelBrowserServiceProvider" php artisan vendor:publish --tag=views --provider="Internetguru\ModelBrowser\ModelBrowserServiceProvider" php artisan vendor:publish --tag=translations --provider="Internetguru\ModelBrowser\ModelBrowserServiceProvider" # If you want to publish everything, you can use the `--provider` option: php artisan vendor:publish --provider="Internetguru\ModelBrowser\ModelBrowserServiceProvider"
Run Tests Locally
To run the tests manually, you can use the following command:
./test.sh
Basic Usage
Show the model browser in your views:
<!-- Base view (cards) --> <livewire:base-model-browser model="App\Models\User" /> <!-- Table view --> <livewire:table-model-browser model="App\Models\User" />
If no viewAttributes are provided, the model's fillable attributes are used by default.
Component Parameters
Both BaseModelBrowser and TableModelBrowser accept the following parameters:
model (required)
The Eloquent model class. Optionally specify a method (scope) to call on the model:
model="App\Models\User" model="App\Models\User@summary"
viewAttributes
Attributes displayed as columns/cards, mapped to their labels:
:viewAttributes="[
'created_at' => __('summary.created_at'),
'name' => __('summary.name'),
'email' => __('summary.email'),
]"
exportAttributes
Attributes included in the CSV export, mapped to their labels. They are hidden in the table and follow the viewAttributes columns, in the order given here. An entry whose key is also a view attribute is exported once, in its exportAttributes position — so a hidden column can be exported next to the visible one it belongs with:
:viewAttributes="['created_at' => __('summary.created_at'), 'customer.name' => __('summary.ordered_by')]" :exportAttributes="[ 'customer.name' => __('summary.ordered_by'), 'customer.email' => __('summary.ordered_by_email'), ]"
The export above has three columns: created_at, customer.name, customer.email.
Only attributes with a single value per row belong here: own columns, or dot paths over to-one relations. A to-many relation has no single value to put in a cell. formats are irrelevant for these columns (nothing renders them); rawFormats apply as usual. Eager-load the relations they reach through via with to avoid an N+1 query per exported row.
formats
Formatting functions for attribute values. Each function receives ($value, $item) and returns the formatted output (HTML is allowed). Values are passed as global function name strings:
:formats="[
'created_at' => 'formatDateTime',
'price' => 'formatCurrency',
'symbol' => 'formatOrderSymbol',
'payment_type' => 'formatTransactionPaymentType',
]"
Define the formatting functions as global helpers, e.g. in a helpers.php file:
function formatDateTime($value, $item) { return \Carbon\Carbon::parse($value)->format('d.m.Y H:i'); } function formatCurrency($value, $item) { return number_format($value / 100, 2) . ' CZK'; }
alignments
Column alignment settings (start, end, or center). Numeric values default to end, others to start:
:alignments="[
'created_at' => 'start',
'amount' => 'end',
'is_active' => 'center',
]"
defaultSortColumn / defaultSortDirection
Default sort when user hasn't selected one:
defaultSortColumn="created_at" defaultSortDirection="desc"
enableSort
Enable/disable interactive column sorting (default: true):
:enableSort="false"
filters / filterSessionKey
See the Filters section below. When using filters, filterSessionKey is required.
refreshInterval
Auto-refresh interval in seconds. When set, the component polls the server and re-renders with fresh data (including total count). Default: 0 (disabled):
:refreshInterval="10"
exportName
The start of a CSV export's file name, followed by the time of the export in the display timezone: vouchers-2026-09-27-1430.csv. Defaults to the model's plural in kebab case (order-items); set it when the list is not simply its model, such as a summary over another one. The filter is not part of the name:
exportName="payments"
exportLimit
Maximum number of rows a CSV export may contain. When the current (filtered) result count exceeds the limit, the download button is disabled and the export endpoint refuses the request. Defaults to the model-browser.export_limit config value (1500). Set to 0 for unlimited:
:exportLimit="2000"
statsAttributes / statsTextAttributes / title / statsLimit
Columns that are summarized, in the table only: their header offers the statistics menu. Every view attribute by default; name some to summarize only those, or pass [] to turn the statistics off. See Column Statistics:
:statsAttributes="['price', 'credit']" :statsLimit="1000"
A column counts as numeric when every value it has is a number, digits in a string included. statsTextAttributes names the columns whose values only look like numbers, such as order numbers; they get no numeric statistics:
:statsTextAttributes="['symbol']"
title names the list as its page does, e.g. :title="__('order.list')"; each menu is headed "Orders / Amount", and a copy of it too. Without it, the heading is just the column's name.
statsLimit is the largest result count the statistics are computed for. Summarizing walks the whole filtered result set, so above it nothing is computed and the menu asks for narrower filters instead. Defaults to the model-browser.stats_limit config value (5000); set to 0 for unlimited.
TableModelBrowser-only Parameters
lightDarkStep
Controls alternating row shading in the table (default: 1):
:lightDarkStep="2"
columnWidths
Custom CSS grid column widths per attribute. Defaults to minmax(4em, 1fr):
:columnWidths="[
'name' => 'minmax(8em, 2fr)',
'email' => 'minmax(10em, 2fr)',
'is_active' => '6em',
]"
Filters
The filter system provides a search bar with Gmail-style query syntax and an expandable filter panel. The active filter lives in the q URL query parameter (see Filter State in the URL), is persisted in the session, and can additionally be initialized from per-filter URL query parameters.
Configuration
Pass an associative array to the filters parameter. Each key is a filter name in kebab case (e.g. created-by), and each value is a config array:
:filters="[
'created' => [
'type' => 'date',
'label' => 'Created',
'column' => 'created_at',
'timezone' => 'Europe/Prague',
],
'symbol' => [
'type' => 'string',
'label' => 'Symbol',
'column' => 'symbol',
'rules' => 'nullable|string|max:20',
],
'voucher' => [
'type' => 'string',
'label' => 'Voucher',
'column' => 'ulid',
'relation' => 'charges.voucher',
'rules' => 'nullable|string|max:32',
'url' => 'voucher',
],
'price' => [
'type' => 'number',
'label' => 'Price',
],
'name' => [
'type' => 'string',
'label' => 'Customer',
'column' => 'name',
'relation' => 'customer',
],
]"
filterSessionKey="order-browser-filters"
Note that price has no column key — it is not auto-applied and must be handled manually in the model scope (see HasModelBrowserFilters Trait).
Filter Config Keys
| Key | Description |
|---|---|
type |
Filter type: string, number, date, options, checkbox (default: string). number and date take a range (see Ranges). Note: checkbox renders a single on/off box whose value is 1 when checked and empty when not — with a column it matches column = 1, without one it is left to the model scope (see HasModelBrowserFilters Trait). |
label |
Display label in the filter panel |
column |
Database column name for auto-apply. When set, the filter is automatically applied to the query. When omitted (and no columns), the filter is NOT auto-applied — use HasModelBrowserFilters trait for manual access. |
columns |
OR group — a list of columns matched with OR instead of a single column (see OR Column Groups) |
relation |
Eloquent relation name — wraps the filter in whereHas(). Supports dot-notation for nested relations. |
options |
Array of options for the options type (e.g. ['value' => 'Label']) |
rules |
Custom Laravel validation rules (overrides default type-based rules) |
url |
URL query parameter name to initialize the filter from (takes priority over session) |
timezone |
Timezone for date filters — the parsed date value is shifted via Carbon::shiftTimezone($tz) (e.g. 'Europe/Prague') |
OR Column Groups
A single filter can match against several columns at once. Use columns instead of column — the filter's value matches when any of the listed columns matches, while the filter as a whole is still AND'd with every other term.
The typical case is collapsing a name + e-mail pair into one input:
'customer' => [ 'type' => 'string', 'label' => 'Customer', 'relation' => 'customer', 'rules' => 'nullable|string|max:60', 'columns' => ['name', ['column' => 'email', 'ascii_fast' => true]], ],
customer:novak then matches customers whose name or e-mail contains novak, and the filter panel shows one input instead of two.
Each columns entry is either a plain column name or an array overriding column, relation, preprocessor, ascii_fast, type or timezone for that column only. Anything not overridden falls back to the filter's own config, so the shared relation above applies to both columns. Columns may also live in different relations:
'party' => [ 'type' => 'string', 'label' => 'Party', 'columns' => [ ['column' => 'name', 'relation' => 'customer'], ['column' => 'name', 'relation' => 'author'], ], ],
All columns of an OR group also take part in free-text search, exactly as separate column filters would.
Ranges
A number or date filter takes a range, its bounds separated by ..:
price:1000..2000 # 1000 to 2000, both included
price:..1000 # up to 1000
price:1000.. # 1000 and more
price:1000 # exactly 1000, the range 1000..1000
created:2026-03-01..2026-03-31
A date bound stands for the whole period it names. The lower bound is the period's first moment and the upper bound its last, so a single value covers the whole period:
created:2026 # the whole year
created:2026-10 # the whole month, also written 10.2026
created:2026-09..2026-10 # September and October
created:2026-10-15 # the whole day, also written 15.10.2026
created:"3 days ago" # that whole day
created:"last month..yesterday" # from the first day of last month to the end of yesterday
created:"2026-10-15 08:00" # a bound with a time is that moment
Relative bounds are anything Carbon::parse() reads (in English), and they span the unit they name: day, week, month, year, hour or minute; today, yesterday and tomorrow are days. Dates are read in the filter's timezone. Each bound is validated on its own against the filter's rules, and a date bound that is not a date is an error.
Both bounds must hold for the same related row, and for the same column of an OR group: with published:2026-03-01..2026-03-31 over posts.published_at, a user with one post before the range and another after it is not listed.
In the filter panel, filters are listed one under another, and a range filter shows two inputs side by side, one for each bound, which are joined into the one value. A date input shows the first or the last day of a bound's period; a bound left untouched keeps what was written, such as 2026-10 or 3 days ago.
Search Query Syntax
The search bar supports Gmail-style syntax:
- Free text:
john— searches across allstring-type filter columns (withcolumnset) - Specific filter:
name:john— applies to thenamefilter - Quoted values:
name:"John Doe"— for values containing spaces - No value at all:
name:— the rows the filter finds nothing on - Range:
price:1000..2000— fornumberanddatefilters (see Ranges) - Combined:
name:john created:2025-01-01..— all terms must match (AND)
Searching for rows with no value
A bare attribute:, followed by a space or the end of the query, matches the rows where the
filter has nothing to match on: a column that is NULL or empty, and, for a filter over a
relation, a row whose relation is missing altogether. With an OR group of columns, a row
qualifies only when none of them carries a value. attribute:"" reads the same, and the
query built from the filter panel writes the bare form.
ordered-by: # orders nobody is named on
paid: novak # unpaid orders, and the free text novak
A bare key that is no configured filter, such as note:, stays free text, and so does an
unfinished name:"Jo. In the filter panel, type "" into a text field to search for no value.
Auto-applied vs Manual Filters
Filters with a column key are auto-applied to the Eloquent query. Filters without column are stored in session but require manual application — useful for custom logic in model scopes:
// Auto-applied filter (no manual code needed): 'name' => ['type' => 'string', 'label' => 'Name', 'column' => 'name'] // Manual filter (applied in your model scope via HasModelBrowserFilters): 'price' => ['type' => 'number', 'label' => 'Price']
Typical reasons to omit column and handle filtering manually:
- The filter operates on a computed/aggregate value (e.g. sum of related records)
- The filter needs custom OR logic across multiple relations
- The filter requires raw SQL expressions
HasModelBrowserFilters Trait
Use this trait in your Eloquent model to access filter values from session for manual filtering. The $modelBrowserFilterSessionKey must match the filterSessionKey passed to the component.
use Internetguru\ModelBrowser\Traits\HasModelBrowserFilters; class Order extends Model { use HasModelBrowserFilters; protected string $modelBrowserFilterSessionKey = 'order_filter'; public static function summary() { $query = static::with(['customer', 'payment', 'charges']); $price = (new static)->getModelBrowserFilterRange('price'); // Manual filter: price is a computed sum of related charges if ($price['from'] !== null) { $query->whereRaw( '(SELECT SUM(amount) FROM charges WHERE charges.order_id = orders.id) >= ?', [$price['from'] * 100] ); } if ($price['to'] !== null) { $query->whereRaw( '(SELECT SUM(amount) FROM charges WHERE charges.order_id = orders.id) <= ?', [$price['to'] * 100] ); } return $query; } }
Available methods:
getModelBrowserFilters()— returns aCollectionof active filter valuesgetModelBrowserFilter(string $key, mixed $default = null)— get a specific filter valuegetModelBrowserFilterRange(string $key)— the bounds of anumberordatefilter as['from' => ?string, 'to' => ?string],nullfor an open one; turn a date bound into its first and last moment withBaseModelBrowser::parseDatePeriod($bound, $timezone)hasModelBrowserFilter(string $key)— check if a filter is sethasModelBrowserFilters()— check if any filters are active
URL-based Filter Initialization
Filters with a url key can be initialized from URL query parameters. When any URL filter is present, session-stored filters are ignored and URL values take priority:
'status' => [ 'type' => 'options', 'label' => 'Status', 'column' => 'status', 'url' => 'filter-status', 'options' => ['active' => 'Active', 'inactive' => 'Inactive'], ]
Link: /orders?filter-status=active
The URL parameters are automatically cleaned from the browser address bar after initialization — the resulting filter is then carried by the q parameter like any other.
Filter State in the URL
The whole search query is mirrored into the q query parameter, so the current filter is part of the URL:
/orders?q=status%3Aactive+name%3A%22John+Doe%22
- The URL is shareable and bookmarkable — opening it applies exactly that filter.
- Every filter change pushes a browser history entry, so back/forward moves between filter states.
- On a plain page load without
q, the filter is restored from the session and the URL is updated to match.
Priority on mount is: per-filter url parameters > q > session. When q is present it fully describes the filter state, so session values are never merged into it — otherwise navigating back would resurrect cleared filters.
Full Example
Below is a complete example of an order browser with auto-applied and manual filters:
<livewire:table-model-browser model="App\Models\Order@summary" filterSessionKey="order_filter" :viewAttributes="[ 'created_at' => __('summary.created_at'), 'symbol' => __('summary.symbol'), 'price' => __('summary.total'), 'customer.name' => __('summary.customer'), 'customer.email' => __('summary.email'), 'payment_accepted_at' => __('summary.paid_at'), 'payment_type' => __('summary.payment_type'), ]" :formats="[ 'symbol' => 'formatOrderSymbol', 'created_at' => 'formatDateTime', 'price' => 'formatCurrency', 'payment_accepted_at' => 'formatDateTime', 'payment_type' => 'formatTransactionPaymentType', ]" :filters="[ 'created' => [ 'type' => 'date', 'label' => __('summary.created'), 'column' => 'created_at', ], 'symbol' => [ 'type' => 'string', 'label' => __('summary.symbol_filter'), 'rules' => 'nullable|string|max:20', 'column' => 'symbol', ], 'voucher' => [ 'type' => 'string', 'label' => __('summary.voucher_filter'), 'rules' => 'nullable|string|max:32', 'url' => 'voucher', 'column' => 'ulid', 'relation' => 'charges.voucher', ], 'price' => [ 'type' => 'number', 'label' => __('summary.price'), ], 'name' => [ 'type' => 'string', 'label' => __('summary.name_filter'), 'rules' => 'nullable|string|max:30', 'column' => 'name', 'relation' => 'customer', ], 'email' => [ 'type' => 'string', 'label' => __('summary.email_filter'), 'rules' => 'nullable|string|max:30', 'column' => 'email', 'relation' => 'customer', ], ]" :columnWidths="[ 'created_at' => 'minmax(7em, 1.2fr)', 'symbol' => 'minmax(8em, 0.5fr)', 'price' => 'minmax(max-content, max-content)', 'customer.name' => 'minmax(7em, 1.2fr)', 'customer.email' => 'minmax(7em, 1.8fr)', 'payment_accepted_at' => 'minmax(7em, 1.2fr)', 'payment_type' => 'minmax(7em, 1fr)', ]" defaultSortColumn="created_at" defaultSortDirection="desc" :enableSort="false" />
In this example:
created,symbol,voucher,name,emailhavecolumnset → auto-applied to the querypricehas nocolumn→ a manual filter handled inOrder::summary()viaHasModelBrowserFiltersvoucherusesrelationwith dot-notation (charges.voucher) for nestedwhereHas()andurlfor URL initialization
Features
- Pagination — One line above the table, reading
1–20 of 176followed by the previous/next buttons, which move byperPage(PER_PAGE_DEFAULT, 20). A Load more results button under the table shows anotherPER_PAGE_STEP(20) rows below the ones already there, up toPER_PAGE_MAXin total. Only the current page grows:perPageis untouched, so the arrows always load a default page and drop the extra rows on the way, and nothing about it is remembered for the next visit. The extra rows are counted inextraRowsand reset by paging and by any filter change. - Auto-refresh — Optional periodic data refresh via
refreshIntervalparameter. - Sorting — Click column headers to sort ascending/descending or reset. Supports default sort column and direction.
- CSV Export — Download the current filtered and sorted data as a CSV file. Exports are capped at
exportLimitrows (per-instance parameter, defaults to themodel-browser.export_limitconfig value of 1500;0disables the cap) — when the current result count exceeds it, the download button is disabled and the export endpoint refuses the request. - Column statistics — Every column carries a statistics menu in its header, loaded inside its own Livewire 4 island so the data query is never re-run for it. See Column Statistics.
- Fullscreen — Toggle fullscreen mode for the table view.
- Copy page — Copy the rows of the current page only to the clipboard, as plain text (TSV) and as an HTML table, ready to paste into a spreadsheet. Cells are copied as they are shown,
formatsapplied and markup left out; the CSV export holds the plainrawFormatsvalues. - Deferred count — The total result count is the
of 176half of the pagination line and is loaded inside a dedicated Livewire 4 island, passed into the pagination component as itscountslot. The table renders immediately from therows()computed property; the count fills in (and refreshes on filter changes) without ever re-running the data query.
Column Statistics
Every column in statsAttributes (all of them by default) carries an icon at the right edge of its header opening a menu of statistics. It takes no room from the column's name, so a narrow column does not break the name mid-word, and it sits in a box of a fixed size, because FontAwesome replaces its <i> with an <svg> only after the page has been laid out. The menu is kept within the visible part of the screen, and headed by the list's title and the column's name. Its first part applies to every column:
| Statistic | |
|---|---|
DISTINCT |
Different values |
EMPTY |
Empty values, and their share of the rows |
NON-EMPTY |
Filled values, and their share of the rows |
EARLIEST / LATEST |
First / last moment, when every value is a date |
A numeric column adds:
| Statistic | |
|---|---|
SUM |
Total of the column's numbers |
AVG |
SUM over COUNT |
MEDIAN |
Middle number, zeros included |
MIN / MAX |
Smallest / largest number, zeros included |
COUNT |
Values, zeros included |
AVGNZ |
SUM over COUNTNZ |
MEDIANNZ |
Middle number that is not zero |
MINNZ / MAXNZ |
Smallest / largest number that is not zero |
COUNTNZ |
Values that are not zero |
An empty value (null or '') is no value at all: every statistic but EMPTY leaves it out, as SQL's AVG(column) and COUNT(column) do. A zero or false is a value. So COUNT less COUNTNZ is the number of zeros. A column whose empty value means zero, and should count as one, has to say so in its summary query (e.g. COALESCE(credit, 0)).
The labels are translated (model-browser::global.stats.labels): in Czech RŮZNÉ, SOUČET, PRŮMĚR and so on. A date span and SUM take the share's column too.
A line divides the groups (BaseModelBrowser::STATS_GROUPS): the ones above, the span of a date column, SUM, which zeros never change, and the statistics counting the zeros, each with its non-zero counterpart beside it (AVG with AVGNZ and so on). A header row names those two columns Non-empty and Non-zero. When a numeric column has no zero, the counterparts would only repeat the rest: the header row reads "No zero values." across the whole row instead, and each value takes both value columns. A column that is not numeric shows none of the numeric groups, COUNT included.
SUM 758,200
Non-empty Non-zero
AVG 611 1,033
MEDIAN 500 1,000
MIN 0 50
MAX 10,000 10,000
COUNT 1,240 734
Below them come the column's ten most frequent values with their count and share of the rows, the most frequent first and ties in the order of the value, and a last row counting the rest ("… 77 other"). A date counts as its day in the display timezone. The values are aligned as their column is in the table: right for a numeric one, unless alignments says otherwise. A value is shown through the column's formats callback, called with the first row it came from, so a formatter can link it; without one, a labelled enum shows its label and a boolean Yes or No. Instead of the list, a note says when there are no values, when all of them are unique, when they are long texts (over 50 characters on average), or too many or of a kind that cannot be counted. A [copy] button under the menu puts all of it on the clipboard as tab-separated lines, as it is shown.
Values are read straight off the model, so they are in the attribute's own unit — formats and rawFormats are display concerns and are not applied while summarizing. The numeric statistics are then rendered through the column's formats callback, which is therefore called with an aggregate and no row ($format($value, null)); one that needs the row it came from falls back to a plain number. The counts are never formatted.
The statistics load when a menu is first opened, with a spinner in the meantime, and then colour every column's icon blue. A list no longer than the model-browser.stats_auto_limit config value (500; 0 turns it off) has them loaded right after its count instead. Every column is summarized in the one pass over the rows. The statistics are kept, also server side, across page changes and sorting, and are discarded when the filters change. Relations the query eager loads, through with or in the model's summary method, are loaded in chunks rather than row by row. Above statsLimit rows none are computed, the icon turns light grey, and the menu reads "To show stats, reduce results below 5,000 using filters."
The statistics are a table feature: the card view (base-model-browser) has no header to carry them.
<livewire:table-model-browser model="App\Models\Order" :viewAttributes="['symbol' => 'Symbol', 'price' => 'Total', 'customer.name' => 'Ordered by']" :formats="['price' => 'formatCurrency']" :statsAttributes="['price']" />
License & Commercial Terms
License
Copyright © 2026 Internet Guru
This software is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) license.
Disclaimer: This software is provided "as is", without warranty of any kind, express or implied. In no event shall the authors or copyright holders be liable for any claim, damages or other liability.
Commercial Use
The standard CC BY-NC-SA license prohibits commercial use. If you wish to use this software in a commercial environment or product, we offer flexible commercial licenses tailored to:
- Your company size.
- The nature of your project.
- Your specific integration needs.
Note: In many instances (especially for startups or small-scale tools), this may result in no fees being charged at all. Please contact us to obtain written permission or a commercial agreement.
Contact for Licensing: info@internetguru.io
Professional Services
Are you looking to get the most out of this project? We are available for:
- Custom Development: Tailoring the software to your specific requirements.
- Integration & Support: Helping your team implement and maintain the solution.
- Training & Workshops: Seminars and hands-on workshops for your developers.
Reach out to us at info@internetguru.io — we are more than happy to assist you!