nakukryskin/orchid-repeater-field
A repeater field for the Orchid Platform that lets you add, sort, and manage repeatable groups of fields inside Laravel admin forms, storing nested data as JSON without writing custom field logic.
The first decision is whether this package belongs in the project at all. nakukryskin/orchid-repeater-field is archived, has roughly 59 stars and no known dependents, and was built against an older Orchid Platform API. For a greenfield admin panel, prefer a small custom Orchid field, a native repeatable structure, or a child table. For a legacy project already pinned to a compatible Orchid version, it can still be a quick way to get “add/remove row” UI without writing JavaScript.
Minimal adoption path
composer.json with your installed orchid/platform version. Because the package is archived, dependency constraints are the most likely blocker. Install on a branch and run composer why-not nakukryskin/orchid-repeater-field if resolution fails.composer require nakukryskin/orchid-repeater-field
php artisan vendor:publish --provider="Nakukryskin\OrchidRepeaterField\OrchidRepeaterFieldServiceProvider"
php artisan view:clear
Confirm the actual provider name in vendor/nakukryskin/orchid-repeater-field/composer.json.src/ for the field class, resources/views/ for the blade template, and resources/js/ for the add/remove-row behaviour. The blade template tells you the exact value shape it expects: usually an array of associative rows, e.g. [['label' => 'Email', 'value' => '[email protected]']].contacts, settings, or meta JSON column is the simplest integration and avoids relationship syncing on day one.Example first use case:
// migration
$table->json('contacts')->nullable();
// model
protected $casts = [
'contacts' => 'array',
];
// Screen layout (namespace/class name may differ by package release)
use Nakukryskin\OrchidRepeaterField\Fields\Repeater;
use Orchid\Screen\Fields\Input;
public function layout(): iterable
{
return [
Repeater::make('contacts')
->title('Contacts')
->fields([
Input::make('label')->title('Label'),
Input::make('value')->title('Value'),
])
->value(old('contacts', $this->contact->contacts ?? [])),
];
}
// store/update handler
$contact->contacts = $request->input('contacts', []);
$contact->save();
The important mental model: the package is a UI widget, not a persistence layer. It renders repeated subfields and posts a nested array. Your application remains responsible for validation, normalization, and saving.
JSON attribute pattern (recommended default). Store the repeater as a JSON column cast to array. The field’s value is an array of rows; each row is an associative array keyed by the subfield names.
$rows = collect($request->input('contacts', []))
->filter(fn ($row) => filled($row['label'] ?? null) || filled($row['value'] ?? null))
->values()
->all();
$contact->contacts = $rows;
$contact->save();
Filtering empty rows is not optional. Users add a row, leave it blank, save, and you end up with [{"label": null, "value": null}]. Normalize before persisting and before validating if blank rows should be ignored.
Validation with wildcards. Validate the posted array with Laravel dot notation, ideally after removing fully empty rows:
$request->validate([
'contacts' => ['array'],
'contacts.*.label' => ['required', 'string', 'max:255'],
'contacts.*.value' => ['required', 'string', 'max:255'],
]);
Keep the validation rules alongside the screen or in a form request. Do not assume the field’s ->required() applies per row; it generally applies to the field as a whole.
Populate on edit and after validation failure. The repeater needs an array value, not a JSON string. Use the model cast and fall back to old() so a failed save does not wipe the row set:
->value(old('contacts', $this->contact->contacts ?? []))
For create screens, seed one empty row if the UX should start with an editable row:
->value(old('contacts', $this->contact->contacts ?? [['label' => '', 'value' => '']]))
Reusable field sets. Define the subfield list once and reuse it across create and edit screens:
private function contactFields(): array
{
return [
Input::make('label')->title('Label'),
Input::make('value')->title('Value'),
];
}
This keeps field names stable, which matters because the posted payload shape depends on them.
Child-table pattern. If repeater rows really represent database records, use the repeater only as an input UI and sync manually in a transaction. Treat the posted array as a form payload, not as a model relation:
DB::transaction(function () use ($request, $contact) {
$contact->update($request->only('name'));
$contact->phones()->delete();
foreach ($request->input('phones', []) as $row) {
if (blank($row['number'] ?? null)) {
continue;
}
$contact->phones()->create([
'label' => $row['label'] ?? null,
'number' => $row['number'],
]);
}
});
For larger datasets, avoid delete-and-recreate; match rows by an id subfield and update/create/delete explicitly.
Testing. Since the field is UI-only, feature-test the screen’s store/update endpoint with a nested array payload and assert the JSON column or child records. That gives you regression coverage even if the package’s JavaScript breaks later.
Archived status is the headline risk. There will be no fixes for newer Orchid releases. If you adopt it, pin versions, commit the lockfile, and consider vendoring or forking the field if you need a patch. Check the package’s field class against Orchid’s current Field base class and rendering contract.
Assets and JavaScript are the usual first failure. If the “Add row” button does nothing, check that package assets were published, that they are not cached by an old manifest, and that the browser console has no JS errors. Run php artisan view:clear and, if applicable, republish assets. Confirm the blade view being rendered is the package’s view and not a stale published override.
Dynamically added rows may not initialize Orchid plugins. Select2, date pickers, uploads, and rich-text editors often bind only on page load. A newly added repeater row can contain plain inputs that work while enhanced fields silently fail. Verify every subfield type you use by adding a row in the browser, not just by reading the code. If needed, hook the package’s add-row event or use a MutationObserver to reinitialize plugins, or restrict subfields to plain inputs.
Index gaps and array keys. Removing rows in the browser can leave non-sequential indexes. Laravel will parse the request, but do not assume 0..n. Use array_values() after filtering, and never rely on the posted index as a stable row identifier.
Double-encoding and cast mistakes. If the column is cast to array, assign an array. Assigning a JSON string can produce double-encoded JSON. Conversely, if there is no cast, the field may receive a string on edit and render incorrectly. Keep the cast and the field value type aligned.
Mass assignment. Add the repeater column to $fillable, or assign it explicitly. A silently ignored attribute looks like the field “did not save.”
Validation and blank rows. Wildcard required rules will fail on empty rows the user added but did not fill. Either filter empty rows before validation or make row fields nullable and enforce “at least one complete row” with a custom rule.
Nested repeaters are risky. Do not assume nested repeater support. If you need hierarchical data, model it as a JSON structure edited by a custom field, or use separate screens/child tables.
Large payloads. A repeater in a JSON column is convenient but not a good fit for hundreds of rows or data you need to query and aggregate. Enforce a maximum row count in validation and move to a child table when the data becomes relational.
Extension points. The practical customization path is publishing and overriding the blade view, subclassing the field class, or forking the package. Before doing that, inspect the package’s view and JS to understand the expected row template and event names. Keep overrides in your app’s resources/views/vendor/... path so upgrades are visible.
Debugging checklist. Dump the raw request with $request->input('contacts') to confirm the nested shape; inspect the generated HTML input names to confirm they are nested (contacts[0][label]); check the network payload; check the browser console; and verify the model cast and saved JSON in the database. Most “repeater bugs” are shape mismatches between the posted array, the field value, and the model attribute.
How can I help you explore Laravel packages today?