Core
Entries
Every entry type an infolist can hold, what state each one expects, and what it renders that state as.
On this page
- TextEntry
- TextEntry API
- BadgeEntry
- IconEntry
- IconEntry API
- BooleanEntry
- ListEntry
- ListEntry API
- ImageEntry
- ImageEntry API
- ColorEntry
- ColorEntry API
- KeyValueEntry
- KeyValueEntry API
- HtmlEntry
- RepeatableEntry
- RepeatableEntry API
- Avoiding N+1 on relation rows
- ChangesEntry
- What it accepts as state
- In context
- ChangesEntry API
- A Custom Entry
- Related
An entry is one fact about the record. Which class you reach for is decided by what the value is — a string, a state with a colour, a boolean, a list, an image, a diff — and every one of them shares the label, icon, colour, size and visibility vocabulary from Foundation.
TextEntry
The default entry. It shares the canonical FormatsState concern with table columns, so money(), numeric(), date(), dateTime(), and since() format a value exactly as the matching TextColumn would.
TextEntry::make('total')->money('Kč'); // 1 234 KčTextEntry::make('weight')->numeric(2); // 1 234,50TextEntry::make('created_at')->date(); // 20.06.2026TextEntry::make('updated_at')->dateTime()->since(); // 3 hours ago TextEntry::make('status')->badge()->color(Color::Success);TextEntry::make('email')->icon('envelope')->copyable();TextEntry::make('bio')->limit(120);TextEntry::make('name')->weight('bold'); // normal|medium|semibold|bold|lightTextEntry::make('notes')->prose(); // long-form, prose stylingTextEntry::make('tags')->bulleted(); // array → bulleted listTextEntry::make('aliases')->listWithLineBreaks(); // array → line-separated list
TextEntry API
| Method | Description |
|---|---|
money(?string $currency = 'CZK') |
Format as currency |
numeric(int $decimals = 0, ?string $decimalSeparator = ',', ?string $thousandsSeparator = ' ') |
Format as a number |
date(?string $format = 'd.m.Y') / dateTime(?string $format = 'd.m.Y H:i') |
Format a date / datetime |
since() |
Render the date as a human diff (diffForHumans) |
badge(bool = true) |
Render the value as a colored pill |
color(string|Color|Closure) |
Badge / text color |
icon(string|Closure, ?string $position) |
Leading icon |
copyable(bool = true) |
Add a copy-to-clipboard affordance |
limit(?int) |
Truncate long text |
weight(?string) |
Font weight |
prose(bool = true) |
Prose styling for long-form text |
listWithLineBreaks(bool = true) / bulleted(bool = true) |
Render an array state as a list |
formatStateUsing(Closure) |
Transform the resolved value ($state, $record) |
BadgeEntry
A first-class TextEntry preset to render as a badge — the ergonomic form of TextEntry::make(...)->badge(). It inherits the full TextEntry API (color, icon, formatting), so the badge chrome stays owned in one place.
use NyonCode\WireCore\Infolists\Components\BadgeEntry; BadgeEntry::make('status') ->color(fn ($state) => $state === 'active' ? Color::Success : Color::Gray) ->icon('check-circle');
IconEntry
Renders an icon derived from the state. Use boolean() for true/false, or icons() for a value → icon map.
IconEntry::make('is_verified')->boolean(); // ✓ success / ✕ danger IconEntry::make('is_active') ->boolean() ->trueIcon('check-badge')->trueColor('success') ->falseIcon('no-symbol')->falseColor('gray'); IconEntry::make('status') ->icons(['draft' => 'pencil', 'published' => 'check', 'archived' => 'archive-box']) ->colors(['draft' => 'gray', 'published' => 'success', 'archived' => 'warning']);
IconEntry API
| Method | Description |
|---|---|
boolean(bool = true) |
Map truthy/falsy state to check/x icons |
trueIcon() / falseIcon() |
Override the boolean icons |
trueColor() / falseColor() |
Override the boolean colors |
icons(array|Closure) |
Map state values to icon names |
colors(array|Closure) |
Map state values to color names |
BooleanEntry
A first-class IconEntry preset to boolean mode — the ergonomic form of IconEntry::make(...)->boolean(). A truthy state renders the success check icon, a falsy state the danger x icon; the icons and colors stay overridable.
use NyonCode\WireCore\Infolists\Components\BooleanEntry; BooleanEntry::make('is_verified'); BooleanEntry::make('is_active') ->trueIcon('check-badge')->trueColor('success') ->falseIcon('no-symbol')->falseColor('gray');
ListEntry
Renders a collection state as a bulleted list or a row of badge chips — the middle ground between a single TextEntry and a full RepeatableEntry. The state may be an array/iterable, or a delimited string split with separator(). Items reuse the TextEntry formatting (number/money/date, formatStateUsing(), limit()).
use NyonCode\WireCore\Infolists\Components\ListEntry; ListEntry::make('tags'); // bulleted list ListEntry::make('tags')->badge()->color('primary'); // badge chips ListEntry::make('roles')->separator(','); // "admin, editor" → two items ListEntry::make('categories')->badge()->limitList(3); // first 3 chips + a "+N" pill
ListEntry API
| Method | Description |
|---|---|
badge(bool = true) |
Render items as badge chips instead of a bulleted list |
bulleted(bool = true) |
Toggle the list bullets (non-badge mode) |
separator(?string) |
Split a scalar string state into items |
limitList(?int) |
Cap the visible items; the rest collapse into a +N indicator |
color(string|Color|Closure) |
Chip / text color |
icon(string|Closure) |
Leading icon on each chip |
ImageEntry
Renders the state as one or more images. Absolute/data URLs are used verbatim; relative paths resolve through the configured disk().
ImageEntry::make('avatar')->circular()->imageSize(56); ImageEntry::make('logo')->disk('public')->defaultImageUrl('/img/placeholder.png'); ImageEntry::make('gallery')->stacked()->imageSize(40); // array state → overlapped gallery
ImageEntry API
| Method | Description |
|---|---|
disk(?string) |
Storage disk for relative paths |
imageSize(int) |
Width/height in pixels |
circular(bool = true) |
Round the image |
stacked(bool = true) |
Overlap multiple images |
defaultImageUrl(?string) |
Fallback when the state is empty |
ColorEntry
Renders a swatch plus the color value, optionally copyable.
ColorEntry::make('brand_color')->copyable();
The formatted value is shown as text, but a swatch is drawn only when it passes
the CSS color allowlist: hex, rgb()/rgba(), hsl()/hsla(), or a bare CSS
color keyword. Unsupported or unsafe values produce no swatch, rather than
being interpolated into a style attribute. copyable() comes from the shared
copyable entry capability.
ColorEntry API
->getSwatch(): ?string // safe CSS color, or null when it is not an accepted color
KeyValueEntry
Renders an array or iterable (including a JSON-cast attribute) as a key/value table. Nested values are JSON-encoded; an empty or non-iterable state renders the entry placeholder.
KeyValueEntry::make('meta') ->keyLabel('Attribute') ->valueLabel('Value');
KeyValueEntry API
->keyLabel(string $label): static // header label — default 'Key'->getKeyLabel(): string->valueLabel(string $label): static // header label — default 'Value'->getValueLabel(): string->getPairs(): array // normalized pairs; non-iterable state becomes []
HtmlEntry
Stored editor content printed as markup rather than as escaped text, with every mention in it resolved on render.
use NyonCode\WireCore\Infolists\Components\HtmlEntry; HtmlEntry::make('body') ->zone('business'); // where mentions link to, when a resource routes per zone
It is a separate entry rather than a flag on TextEntry on purpose: a text entry escapes and must keep escaping. Printing markup is a decision an application makes deliberately, about a column it controls — so it is a different name, not a switch that quietly turns escaping off.
Mentions are read back, not replayed. A @person or #article stored in the
document carries an identity, not a name and a link, so the entry resolves each
one on every render: a renamed record reads renamed here, and a mention whose
record is gone (or that the viewer may not see) degrades to plain text instead of
a dead link. zone() is what a page that read Zone::current() in mount()
passes on, so those links land in the mount point the reader is already in;
without it they link through the default zone. See
TiptapEditor → Mentions for the writing
half.
->zone(?string $zone) // the mount point mention links resolve in->getRenderedHtml(): string // the document with every mention resolved
RepeatableEntry
Renders a nested entry schema once per item of an iterable state — a hasMany relation or an array of rows.
RepeatableEntry::make('items') ->columns(3) ->schema([ TextEntry::make('label')->weight('medium'), TextEntry::make('price')->money('Kč'), TextEntry::make('qty')->numeric(), ]);
RepeatableEntry API
->schema(array $components): static // Entry objects rendered for each item->getSchema(): array->columns(int $columns): static // grid columns per item — default 1->getColumns(): int->contained(bool $condition = true): static // wrap each item in a bordered card — default true->isContained(): bool->with(array|string $relations): static // eager-load relations; repeated calls merge without duplicates->getWith(): array->getRowItems(): array // list of state items re-indexed from zero->getRows(): array // list of cloned child entries bound to each row item
Per-row actions(array $actions) is inherited from the shared entry API; see
Infolist actions.
Avoiding N+1 on relation rows
When the rows are Eloquent models whose child entries read a nested relation path (e.g. product.name on each order line), reading that path lazily loads the relation once per row — an N+1. Declare the relations with with() and they are eager-loaded across every row in a single query before rendering:
RepeatableEntry::make('lines') ->with(['product', 'tax']) // one query per relation, not per row ->schema([ TextEntry::make('product.name'), TextEntry::make('tax.rate')->numeric(2), ]);
with() is a no-op for array rows and merges across repeated calls. (The relation that backs the repeatable itself — lines — should be eager-loaded on the parent query as usual.)
ChangesEntry
Renders a before-and-after diff as one table — a row per field that moved, the old side and the new side beside each other and coloured, with one set of headings for the whole diff.
ChangesEntry::make('changes');
Reach for it wherever something records what changed: an audit entry, a revision, the report of a sync. It is what the audit module's entry page draws, and what the audit trail slide-over draws inside a record — the same table, so a diff reads the same in both places.
What it accepts as state
Two shapes, and it tells them apart itself, because a caller has one of them and both mean the same thing:
- the
{old, new}map an audit entry produces —['status' => ['old' => 'draft', 'new' => 'sent']] - rows that are already
{field, before, after}
Either way the values go through NyonCode\WireCore\Foundation\ValueObjects\ChangeSet, which owns how a stored value reads: null stays null so the renderer can say (empty), a boolean is written true / false rather than 1 and nothing, and an array is JSON with its slashes and accents left alone.
That single owner is the point. The rule used to be written twice — once in the audit module and once in a Blade ternary in the trail — and the copy nobody could reach from a test printed booleans as 1.
In context
use NyonCode\WireCore\Foundation\Schema\Section;use NyonCode\WireCore\Infolists\Components\ChangesEntry;use NyonCode\WireCore\Infolists\Components\TextEntry; public function infolist(Infolist $infolist): Infolist{ return $infolist->schema([ Section::make('what-happened')->label('What happened')->columns(2)->schema([ TextEntry::make('event')->badge(), TextEntry::make('created_at'), ]), Section::make('changes')->label('Changes')->schema([ ChangesEntry::make('changes') // The state is a diff, never a record: hand it the pairs, // or rows you already built. ->state(fn ($record): array => $record->getChangeDiff()) ->hiddenLabel() ->placeholder('No field changed.'), ]), ]);}
dense() draws it a size smaller, for a diff that is supporting evidence inside something else rather than the subject of the page — which is how the audit trail slide-over draws it.
ChangesEntry API
->dense(bool $dense = true) // smaller type, for a diff inside a slide-over or timeline->isDense(): bool->getRows(): array // [['field' => string, 'before' => ?string, 'after' => ?string], ...]
Everything else — label(), hiddenLabel(), state(), placeholder(), columnSpan(), visible() — is the shared entry surface documented above.
A Custom Entry
For a value none of the entries above draws, write an entry class and its view:
php artisan make:wire-entry Money
writes app/Infolists/Components/MoneyEntry.php — an Entry whose only
obligation is viewName() — and the view it names:
namespace App\Infolists\Components; use NyonCode\WireCore\Infolists\Components\Entry; class MoneyEntry extends Entry{ protected function viewName(): string { return 'infolists.entries.money'; }}
{{-- resources/views/infolists/entries/money.blade.php --}}@php $value = $field->getFormattedState();@endphp <div class="{{ $field->getColumnSpanClass() }}" @wireExtraAttributes($field)> @if($field->hasVisibleLabel()) @include('wire-core::partials.entry-label', ['text' => $field->getLabel()]) @endif <div class="text-sm text-gray-900 dark:text-white"> @if($value !== '-' && $value !== '') {{ $value }} @else <span class="text-gray-400">{{ $field->getPlaceholder() ?? '-' }}</span> @endif </div> @if($field->hasActions())@include('wire-core::infolists.entry-actions')@endif </div>
The view receives the entry as $field and draws the label, column span, extra
attributes and entry actions through the same partials the built-in entries
use, so it lines up with them. Read the state through
getFormattedState(), which applies formatStateUsing() and the rest. An
existing view is never overwritten, --force replaces the class only, and
php artisan vendor:publish --tag=wire-core::stubs lets you change what it
writes.
Related
- Infolists — the object these are composed into
- Actions — putting a button beside them
- Columns — the table counterparts, sharing the same surfaces
- Editable Panels — the entries that write back