mxms/checklist
mxms/checklist is a Laravel package for building and managing checklists and their items. It offers a simple, model-friendly way to add tasks, mark them complete, and track progress. Use it to organize to-do lists, onboarding steps, or repeatable workflows.
Install into a Laravel 9/10 app that already runs Nova 4:
composer require mxms/checklist
Before writing any code, open vendor/mxms/checklist/src and vendor/mxms/checklist/dist. The README on this fork is thinner than the original e2consult/novachecklists docs, and the class namespace may still be the upstream one (E2Consult\NovaChecklists\Checklist) rather than Mxms\.... Grep for class Checklist and copy the use statement from there — don't guess it.
1. Add the storage column. A json column is the least surprising choice:
Schema::table('projects', function (Blueprint $table) {
$table->json('checklist')->nullable();
});
2. Cast it so the field receives a real array instead of a string:
protected $casts = [
'checklist' => 'array',
];
3. Register the field on a Nova resource. Give every option a stable machine key and a separate human label:
use Mxms\Checklist\Checklist; // verify against vendor/
Checklist::make('Onboarding', 'checklist')
->options([
'contract_signed' => 'Contract signed',
'kickoff_done' => 'Kickoff call held',
'repo_created' => 'Repository created',
'billing_set' => 'Billing configured',
]),
First use case: an onboarding/setup checklist on a Project resource, visible on the detail page, where "done" means "the key is present in the stored array". Keep the option list in one place (a config file or a static method on the resource) from day one — you will immediately want to reuse it to compute a progress percentage, and duplicating the list is how these fields rot.
Single source of truth for options + progress. Define the list once, then derive completion on the model:
// config/checklists.php
return ['project' => [
'contract_signed' => 'Contract signed',
'kickoff_done' => 'Kickoff call held',
'repo_created' => 'Repository created',
]];
// app/Models/Project.php
public function getChecklistProgressAttribute(): int
{
$all = array_keys(config('checklists.project'));
$done = array_intersect($all, $this->checklist ?? []);
return $all ? (int) round(count($done) / count($all) * 100) : 0;
}
Then in the resource ->options(config('checklists.project')).
Dynamic options from the database (per-tenant checklists, templates, etc.):
Checklist::make('Checklist')
->options(
ChecklistStep::orderBy('position')->pluck('label', 'key')->all()
),
Because only keys are persisted, renaming a step later doesn't corrupt existing records — this is the main reason to prefer DB-driven options over hardcoded labels.
Index column that doesn't dump raw JSON. The default index rendering is the raw stored value, which looks terrible. Either hide it or format it:
Checklist::make('Checklist')
->options(config('checklists.project'))
->onlyOnDetail();
// or, on the index:
->displayUsing(fn ($value) => count($value ?? []) . ' / ' . count(config('checklists.project')) . ' done')
Programmatic toggling via Actions. Useful for "mark complete" buttons that don't require opening the form:
Action::make('Mark kickoff done')
->action(fn (Project $p) => $p->update([
'checklist' => array_values(array_unique(
[...($p->checklist ?? []), 'kickoff_done']
)),
])),
Derived status field. Pair the checklist with a read-only badge so you can filter on it:
Text::make('Setup', fn () => $this->checklist_progress . '%')
->exceptOnForms()
->sortable(), // sorts the real column via a ->sortable(fn ($q) => ...) callback, not the JSON
Read-only review mode. Put the field on detail only, or ->readonly() for roles that should see but not toggle. Wrap writes in your policy — the field has no authorization of its own.
"mxms/checklist": "1.0.0") and commit composer.lock. Plan for the maintainer disappearing: the field is small enough to copy src/ and dist/ into app/Nova/Fields/ and own it. That is a one-afternoon insurance policy.dist/js/field.js; small forks routinely ship without it. If the field renders as an empty box, cd vendor/mxms/checklist && npm install && npm run prod (or php artisan nova:publish) before you spend hours debugging your resource.json/text but the model lacks the array cast, you'll store "[\"a\",\"b\"]" and the checkbox state never round-trips. Column type and cast must agree. If a cast conflicts with other code, decode defensively: is_string($v) ? json_decode($v, true) : $v.null on fresh records. count($model->checklist) throws on an unset column. Always ?? [] or cast with a default.->sortable() or ->searchable() on the raw column. MySQL sorts the entire JSON blob lexically, and LIKE '%key%' matches any substring. If you need it, add a real generated column or denormalized progress integer maintained by an observer.Rule::in(array_keys($options)) so a removed option can't linger in the column forever.project_checklist_items pivot if that's your use case.Field, so ->meta([...]) passes extra config through to the Vue component, and subclassing lets you override component or resolveUsing if you need custom rendering without forking the JS.$project->fresh()->checklist), not assertDatabaseHas on the raw string — key order and whitespace in the JSON encoding are not guaranteed.How can I help you explore Laravel packages today?