Skip to content

Own widgets

Terminal window
php spark make:anvil-widget OpenInvoices

creates App\Widgets\OpenInvoices and the view app/Views/widgets/open_invoices.php. Options: --namespace, --suffix (class name OpenInvoicesWidget), --force. See Commands.

A widget implements Anvil\Widget\Widget:

Method Purpose
canView(Gate $gate): bool whether the current user sees the widget
columnSpan(): int width on extra large screens (the grid clamps it to 2–12)
render(): string the HTML of the widget; it only runs for widgets that are shown
namespace App\Widgets;
use Anvil\Security\Authorization\Gate;
use Anvil\Widget\Widget;
use App\Models\InvoiceModel;
final class OpenInvoices implements Widget
{
public function canView(Gate $gate): bool
{
return $gate->allows('invoices.view');
}
public function columnSpan(): int
{
return 6;
}
public function render(): string
{
// Load data here: render() runs only when the widget is shown.
$invoices = (new InvoiceModel())->where('paid_at', null)->orderBy('due_on')->findAll(5);
return view('widgets/open_invoices', ['title' => 'Open invoices', 'invoices' => $invoices], ['saveData' => false]);
}
}
<?php // app/Views/widgets/open_invoices.php ?>
<div class="card h-100">
<div class="card-body">
<h2 class="anvil-widget-title"><?= esc($title) ?></h2>
<ul class="list-unstyled mb-0">
<?php foreach ($invoices as $invoice): ?>
<li><?= esc($invoice['number']) ?> · <?= esc($invoice['due_on']) ?></li>
<?php endforeach ?>
</ul>
</div>
</div>

Return it from Panel::widgets() (new OpenInvoices()) or Resource::listWidgets().

  • Escape every value; no inline scripts, style attributes or event attributes (CSP), see the rules for extensions.
  • card h-100 gives the same look and height as the built-in widgets; anvil-widget-title is the heading style.
  • A widget that renders a <canvas data-anvil-chart="…"> gets the chart script loaded like ChartWidget; reuse ChartWidget inside your widget rather than building the attribute yourself.
  • Implement Anvil\Widget\Layout\LayoutWidget (layoutKey(), layoutLabel()) – or use the trait HasLayoutKey with a $label property – so users can arrange the widget by a stable key; others are keyed by their position.
  • Widgets are not interactive by themselves. For buttons or forms that change data, render a component inside the widget.

An own widget follows the search and filters of a list by implementing Anvil\Widget\ListAware as well:

Method Purpose
followsList(): bool true if the widget uses the records of the list
forList(Closure $records): static receives fn (): Model, a fresh query of the list for every call; return a copy of the widget that uses it
use Anvil\Data\Model;
use Anvil\Security\Authorization\Gate;
use Anvil\Widget\ListAware;
use Anvil\Widget\Widget;
use Closure;
final class ListedAmount implements ListAware, Widget
{
/** @var (Closure(): Model)|null */
private ?Closure $records = null;
public function followsList(): bool
{
return true;
}
public function forList(Closure $records): static
{
$copy = clone $this;
$copy->records = $records;
return $copy;
}
public function canView(Gate $gate): bool
{
return $gate->allows('invoices.view');
}
public function columnSpan(): int
{
return 4;
}
public function render(): string
{
$row = $this->records === null ? null : ($this->records)()->selectSum('amount', 'total')->asArray()->first();
$total = number_format((float) ($row['total'] ?? 0), 2);
return view('widgets/listed_amount', ['title' => 'Amount in this list', 'total' => $total], ['saveData' => false]);
}
}