digital-creative/value-widget
A Filament widget for displaying a single value or statistic with customizable formatting, styling, and layout options. Drop it into any dashboard or page to surface key metrics, counts, or computed figures at a glance, with support for labels, icons, and descriptions.
Install (pin at least ^1.0.2 — earlier 1.0.x had a reactivity bug, see Gotchas):
composer require dcasia/value-widget:^1.0.2 # verify vendor/name against Packagist; repo is dcasia/value-widget
php artisan vendor:publish --tag=value-widget-assets # verify tag name against the repo
php artisan nova:publish
Read the source first. Small package, thin API surface — the README plus src/ is a 10-minute read. Confirm:
DigitalCreative\ValueWidget\ValueWidget vs. a Nova Metric subclass),calculate() or value()),$component string used for the Vue card, so your Blade/vue assets line up.Register a widget on your dashboard (Nova 4 Dashboard class, or a dcasia/nova-dashboard layout if you use that package):
public function cards(): array
{
return [
new \App\Nova\Metrics\ActiveSubscribers,
];
}
First use case: the honest one — a single "headline number" tile (active users, revenue this month, open tickets). Build it once, verify formatting and range switching, then clone it for the rest of your KPIs.
ActiveUsers, MonthlyRevenue) rather than parameterising a single widget with constructor args. It plays nicer with Nova's navigation and cache-key generation, and keeps calculate() testable.calculate() returning a plain scalar (or the package's value/delta object), and leave formatting (currency, thousands separators, suffix) to the widget's display config or the Vue component. This lets you unit-test the number without booting Nova.$ranges = [30, 60, 90] or ->ranges()), implement the range argument in calculate() explicitly and cover each branch — range handling in small widgets is where silent bugs live. As of v1.0.2 the card should now re-render on range change; if you're pinned below that, verify switching ranges actually refreshes the number before trusting it.public function calculate(NovaRequest $request)
{
return cache()->remember(
'active-users:'.$request->range,
now()->addMinutes(5),
fn () => User::where('last_seen', '>=', now()->subDays($request->range))->count()
);
}
Note that the package's own cacheFor() (if present) may or may not respect the range — verify, otherwise different ranges collide on one cache key.->suffix()/->format() plus a conditional colour so the visual doesn't mislead ops users.calculate() returns the expected scalar with Carbon::setTestNow() and a seeded factory dataset, and assert per-range behaviour. Do not try to test the Vue side; there's not enough package surface to justify it.v1.0.2 landed (reactivity fix, PR #4). Small win, but it's a single-commit patch on a low-star package — still pin the exact version in composer.json and be prepared to fork. The class is small; if it breaks on a Nova major, vendor it into app/Nova/Metrics and maintain it yourself.>=1.0.2 if you switch ranges. The v1.0.2 change ("Fix reactivity issue") means earlier 1.0.x versions can fail to update the displayed value reactively — most visibly when changing the range selector or refreshing the dashboard, where you see a stale number with no PHP error. If you inherited ^1.0.0, bump and re-publish assets before debugging your query.nova:publish wasn't re-run after upgrading. Add it to your deploy script rather than remembering it manually.cacheFor() is implemented without incorporating $request->range (or the user), all ranges/users share one number, and the bug only appears after the first cached result expires. Reproduce with cache cleared before assuming the query is wrong — and note this masquerades as a "reactivity" problem when it isn't.created_at against local now(), daily numbers will be off by your offset near midnight. Use now()->utc() inside calculate() and verify at the boundaries.selectRaw aggregate over Eloquent collection counting.jsonSerialize() or swap the $component for your own Blade/vue card. That's the clean escape hatch when you need something the widget doesn't expose.cards(), (3) calculate() returns a non-null scalar for the current range, (4) policy/authorisation on the dashboard, (5) cache key includes the range, (6) browser console for the Vue error.How can I help you explore Laravel packages today?