davidpiesse/nova-toggle
Add a boolean toggle switch field to Laravel Nova resources with davidpiesse/nova-toggle. Display on/off states as a compact, interactive switch in forms and index views, perfect for flags, active status, and simple true/false settings.
Before you type composer require. This package is a Nova field and nothing else — no Blade directive, no Livewire component, no standalone JS. If you aren't running Laravel Nova, close the tab. If you are, the next question is which Nova: the last release is 2022-07-22, which puts it in Nova 4 / Vue 2 territory. Nova 5's Vue 3 + Vite pipeline is the documented failure mode and it fails silently. Confirm your major version first.
composer require davidpiesse/nova-toggle
Then inspect what landed before writing a single field. This is a ~100-line package; five minutes of reading saves you a debugging afternoon:
cat vendor/davidpiesse/nova-toggle/composer.json # explicit laravel/nova constraint, or wildcard?
ls vendor/davidpiesse/nova-toggle/src # Toggle.php + service provider
ls vendor/davidpiesse/nova-toggle/dist # prebuilt bundle committed? Vue 2 or Vue 3?
Read src/Toggle.php first and confirm it extends Boolean — not a reimplementation. If it extends Boolean, you inherit default(), rules(), nullable(), trueValue(), falseValue(), fillUsing(), onlyOnForms(), exceptOnForms() for free, and field semantics stay identical to core. If it reinvents them, stop and reconsider. Second, check the provider's boot() for Nova::script() / Nova::style() calls naming the bundle. Third, look at dist/ — whether the compiled asset exists and which Vue major it targets is the single answer that decides adopt-versus-fork.
First use case. Pilot on one non-critical resource — a settings page, a taxonomy table, a CMS flag — never on users.is_active first. Swap exactly one field:
// app/Nova/Setting.php
use Davidpiesse\NovaToggle\Toggle;
public function fields(NovaRequest $request)
{
return [
// Boolean::make('Published', 'is_published'),
Toggle::make('Published', 'is_published'),
];
}
No migration, no schema change, no data remediation. The column stays boolean/tinyint(1). Rollback is reverting the import and the make() call — which is why the pilot is cheap and why you should never bundle the swap with a schema or policy change in the same PR.
Verify all four surfaces before rolling out further: index cell, detail view, create form, update form. Then, if you use inline index editing, verify that too — it's the surface most likely to break, because it exercises the component's update path rather than just its render path. Confirm persistence on create and update, and confirm default() and rules() behave exactly as Boolean did. Open DevTools → Console while you do this: a broken Vue mount produces a 200, a clean log, and an empty field.
The realistic day-to-day work here is mechanical: grep -r "Boolean::make" app/Nova, classify each field, swap, one PR per resource. Keep a written inventory of which fields are now toggles — that inventory becomes your bounded QA checklist on every future Nova upgrade.
Toggle::make('Featured', 'is_featured'),
Toggle::make('Verified', 'is_verified'),
The discipline that matters: the toggle is a view layer only. Never let business logic live in the field definition. Audit trails, permission gates, and optimistic rollback belong in the model, an observer, or a Nova action:
// app/Observers/ProductObserver.php — audit trail lives here, not in the field
public function updated(Product $product): void
{
if ($product->wasChanged('is_published')) {
ActivityLog::record('product.published', $product, [
'from' => $product->getOriginal('is_published'),
'to' => $product->is_published,
]);
}
}
Inline index toggling makes accidents cheaper but not less likely — a misplaced click on a listing now flips is_published in production. That's precisely why the observer above exists. Policies still gate the request; human risk is what changed.
If you have dozens of boolean flags, don't scatter Toggle::make() calls across resources. A small factory keeps labels and colors consistent and gives you a one-line kill switch back to Boolean:
// app/Nova/Fields/Flag.php
namespace App\Nova\Fields;
use Davidpiesse\NovaToggle\Toggle;
use Laravel\Nova\Fields\Boolean;
class Flag
{
public static function make(string $name, ?string $attribute = null)
{
if (! config('nova.toggles_enabled')) {
return Boolean::make($name, $attribute ?? $name);
}
return Toggle::make($name, $attribute ?? $name);
// README documents on/off labels and colors — verify exact method names
// against src/Toggle.php before chaining, e.g. ->onColor(...)->offColor(...)
}
}
This is the highest-leverage pattern in the package: it gives you a config-flag rollback to plain checkboxes without touching every resource, which is exactly what you want if a Nova minor upgrade breaks the Vue component mid-sprint.
Because the class extends Boolean, Nova's inherited scoping methods work normally:
Toggle::make('Published', 'is_published')
->exceptOnForms(), // index/detail only — flip without opening the form
Toggle::make('Locked', 'is_locked')
->onlyOnDetail()
->help('Locked accounts cannot sign in.'),
exceptOnForms() on the index is the pattern that actually delivers the package's stated value: ops staff flipping a flag straight from the list view. Be honest with yourself — if your team never edits from the index, the marginal benefit over Boolean drops sharply and a checkbox is fine.
Boolean where it's the right toolDon't sweep blindly. Retain Boolean for:
nullable() with a meaningful "unset" state is a checkbox-with-indeterminate problem, not a two-position switch problem. Keep Boolean until you've explicitly tested null round-tripping through the toggle.trueValue() / falseValue() — e.g. 'yes'/'no' or 1/0 against a string column. Verify inheritance before assuming it works.Automated tests won't catch a broken Vue component. What you can assert server-side is persistence and authorization:
it('persists the toggle attribute on update', function () {
$product = Product::factory()->create(['is_featured' => false]);
$this->actingAs($admin)
->putJson("/nova-api/products/{$product->id}", ['is_featured' => true])
->assertOk();
expect($product->fresh()->is_featured)->toBeTrue();
});
That proves persistence and policy — not rendering. Rendering requires a visual QA step in the release checklist. A blank field, a raw input, or an empty cell all render with a 200 and zero server-side error.
app/Nova.default() / rules() parity with Boolean.acme/nova-fields package.Nova 5 / Vue 3 is the real risk, not code quality. A dist/ compiled for Vue 2 mounted into a Vue 3 Nova produces an empty field with no server-side error — the worst failure mode, because logs are clean and data is intact. A silent console error at component registration is your only signal. Open the browser console on the pilot page; don't rely on the test suite or on storage/logs.
Pin the exact version and record the validated Nova version in the repo. "davidpiesse/nova-toggle": "1.2.3" — not ^1.0. Write the validated Nova version into docs/nova.md or a composer comment, because the person doing the next Nova upgrade will not know this package is load-bearing.
An absent or wildcarded laravel/nova constraint is a red flag, not a convenience. It means the package never declared what it works with. Escalate that to a deliberate "pin and pilot" decision rather than accepting it.
Prebuilt dist/ vs. building from source. If a compiled bundle is committed, you get a free pass — unless you need to patch behaviour, at which point you inherit a permanent build obligation against your own Nova asset pipeline. Decide that before adopting, not after. If you must build, budget CI time alongside the fork.
Beware stale published assets. php artisan vendor:publish / nova:publish copies can shadow the vendor bundle and mask an upgrade. If the field looks right before a Nova bump and broken after, check for a stray published copy under public/vendor/ before debugging the package.
Component-name collisions. Nova field packages register Vue components by name. Install another toggle-ish field and two registrations under the same name will silently win-or-lose depending on boot order. If you fork, namespace the component name.
Treat an unusable is_active toggle as an outage, not a cosmetic bug. If ops can't reach the control, a live workflow is blocked. Route detection through visual QA, and keep the field inventory so you know exactly which workflows to spot-check after any Nova change.
Debugging checklist when the field renders blank:
# 1. Is the asset being served at all?
curl -I https://yourapp.test/vendor/nova-toggle/js/field.js
# 2. Did the service provider actually boot?
php artisan about | grep -i toggle
# 3. Rebuild Nova's asset manifest / clear published caches
php artisan nova:publish --force && php artisan view:clear
Then DevTools → Console on the resource index. [Vue warn]: Failed to resolve component or an ES-module syntax error both point at a Vue-major mismatch, not at your field configuration.
Assume unmaintained on day one. Single maintainer, no release since 2022, zero dependents, community-only support — Nova's paid support does not extend to third-party fields. Write the fork procedure and the escalation contact into the adoption record now, so the decision isn't invented mid-incident. Given the size — one PHP class, one Vue component, one asset — forking is hours, not weeks.
Extension points, in order of preference: (1) chain Nova's inherited field methods; (2) subclass Toggle in app/Nova/Fields/ to add your own config helpers; (3) fork into an internal acme/nova-fields package if you need to patch the Vue component or when Nova's next major breaks it. Option 3 removes single-maintainer exposure permanently and is the sane endgame for a hundred lines of code you'd otherwise re-verify on every Nova upgrade forever.
How can I help you explore Laravel packages today?