K

Core

ListWidget

A short feed of recent records — the widget that answers "which ones" after a stat has said how many, and the item each entry is built from.

On this page

The panel a dashboard reaches for after its figures. "37 new orders" is the stat; which orders is this. Ten rows of title, detail and timestamp — the last few records, the last few things that happened.

use NyonCode\WireCore\Widgets\ListWidget;

How It Works

Why not a table. TableWidget answers a bigger version of the same question and brings a table's whole apparatus with it: columns, sorting, pagination, a toolbar, a Livewire component of its own. Ten rows of title-and-timestamp need none of that, and a table shrunk until it fits a dashboard card is still a table. Reach for TableWidget when the reader needs to work with the rows; reach for this when they need to see them.

Pure markup. No JavaScript, no canvas, no round trip to draw. An entry with a url is a real <a>, so the feed works with JavaScript off and a middle-click opens a tab.

The link covers the whole entry. A url turns the row itself into the anchor rather than adding one inside it — a link wrapped around only the title gives a pointing device a target three pixels tall. newTab() carries rel="noopener noreferrer" with it, which is not decoration: without noopener the opened page can reach back through window.opener.

Colour tints the disc, not the text. An entry's colour goes on the icon's disc. A feed is read as a column of sentences, and colouring the sentences turns it into a rainbow; colouring a 32-pixel disc beside each one keeps "failed" scannable while the text stays text. Both classes come from the canonical palette, so a caller-supplied name never reaches Tailwind.

Where the series comes from. items() takes an array or a closure. The closure receives the active filter key and runs on every render — see Filters.

The trap. There is no maxItems(). A widget that trimmed the series would let a caller load a thousand rows to draw five; the place that knows how many are wanted is the query, so the limit goes there.

Basic Usage

ListWidget::make()
->heading('Recent orders')
->items([
ListItem::make('#1042')->description('Acme s.r.o.')->meta('2 minutes ago'),
ListItem::make('#1041')->description('Bravo a.s.')->meta('18 minutes ago'),
])
ListItem::make('#1042')
->description('Acme s.r.o.')
->url(route('orders.show', $order))
->newTab() // target="_blank" + rel="noopener noreferrer"

Status At A Glance

An icon on a tinted disc is what makes a feed scannable without reading it:

ListItem::make('Payment failed')
->description('Order #1042 — card declined')
->icon('outline:exclamation-triangle')
->color('danger')

Dividers

Hairlines between entries are on by default. Turn them off for a card that is already dense:

ListWidget::make()->items($items)->dividers(false)

Empty State

ListWidget::make()
->items(fn () => $this->recentOrders())
->emptyState('No orders yet today.')

Passing null restores the default (Nothing to show., translated).

Extended Example

namespace App\Dashboards;
 
use App\Models\Order;
use NyonCode\WireCore\Widgets\Dashboard;
use NyonCode\WireCore\Widgets\ListItem;
use NyonCode\WireCore\Widgets\ListWidget;
 
class OperationsDashboard extends Dashboard
{
public function widgets(): array
{
return [
ListWidget::make()
->heading('Recent orders')
->description('The last ten, newest first')
->pollingInterval('30s')
->items(fn () => Order::with('customer')
->latest()
->limit(10) // the limit belongs to the query
->get()
->map(fn (Order $order) => ListItem::make($order->reference)
->description($order->customer->name)
->meta($order->created_at->diffForHumans())
->icon($order->status->icon())
->color($order->status->color())
->url(route('orders.show', $order)))
->all())
->emptyState('No orders yet today.'),
];
}
}

ListWidget API

->dividers(bool $condition = true) // hairline between entries — default true
->hasDividers(): bool

Everything else a list widget takes is shared and documented centrally: items(), emptyState(), filter(), lazy(), heading(), columnSpan(), pollingInterval() and the authorization setters are on the widget base.


ListItem

One entry: what happened, optionally when, optionally where to go to see it.

use NyonCode\WireCore\Widgets\ListItem;

Full Example

ListItem::make('Order #1042')
->description('Acme s.r.o. — 12 400 Kč')
->meta('2 minutes ago')
->icon('outline:shopping-cart')
->color('success')
->url(route('orders.show', $order))
->newTab()
->extraAttributes(['data-testid' => 'order-1042'])

ListItem API

Built with ListItem::make(string $title).

->description(?string $description) // secondary line under the title
->meta(?string $meta) // right-aligned aside — a timestamp, a count, a status word
->icon(string|Icon|null $icon) // icon on the tinted disc
->color(?string $color) // any palette color key — tints the disc
->url(?string $url) // makes the whole entry a link
->newTab(bool $condition = true) // open it in a new tab, with rel="noopener noreferrer"
->extraAttributes(array $attrs) // custom HTML attributes on the entry
->getTitle(): string
->getDescription(): ?string
->getMeta(): ?string
->getIcon(): ?string
->getColor(): ?string
->getUrl(): ?string
->opensInNewTab(): bool
  • Tables And Custom ViewsTableWidget, when the rows have to be worked with rather than read
  • Progress — the other pure-CSS widget, for figures heading toward a target
  • Notifications — the other feed in this framework, and a different one
  • Widgets — the shared base: polling, filters, deferral, authorization