Collapse Repeaters based on $state or with JS

Hello. I have "Orders". They have many "Items" so I use Repeater::make. I guess you can't use $state since Collapsible applies to the repeater not to each individual item. So how can I collapse each item individually based on $state['done']. EDIT: Actually I want them to be collapsed (or automatically collapsed onLoad) if done=true, not collapse them with a button. In the browser, the "done" filed is a switch, so... I've tried with JS, but can't figure how to. I was able to get this far using ChatGPT, but I think it does a worse job than I do:
document.addEventListener('livewire:navigated', () => {
const statePath = 'item';

function procesarToggle(toggle) {
const idAttr = toggle.getAttribute('id');
const match = idAttr.match(/record-\d+/);

if (!match) return;
const itemKey = match[0];

const checked = toggle.getAttribute('aria-checked') === 'true';

if (checked) {
window.Livewire.dispatch('repeater::collapseItem', { statePath, itemKey });
} else {
window.Livewire.dispatch('repeater::expandItem', { statePath, itemKey });
}
}

function configurarEventos() {
const toggles = document.querySelectorAll(`button[id^="data.${statePath}"][id$=".done"]`);

toggles.forEach((toggle) => {
procesarToggle(toggle);
});
}

configurarEventos();
});
document.addEventListener('livewire:navigated', () => {
const statePath = 'item';

function procesarToggle(toggle) {
const idAttr = toggle.getAttribute('id');
const match = idAttr.match(/record-\d+/);

if (!match) return;
const itemKey = match[0];

const checked = toggle.getAttribute('aria-checked') === 'true';

if (checked) {
window.Livewire.dispatch('repeater::collapseItem', { statePath, itemKey });
} else {
window.Livewire.dispatch('repeater::expandItem', { statePath, itemKey });
}
}

function configurarEventos() {
const toggles = document.querySelectorAll(`button[id^="data.${statePath}"][id$=".done"]`);

toggles.forEach((toggle) => {
procesarToggle(toggle);
});
}

configurarEventos();
});
The image below is from my OrdersResouce.php
No description
Solution:
Hey, i looked into Laravel 4.x docs and they added more utilities. Looking at the https://filamentphp.com/docs/4.x/forms/repeater#collapsing-items table, nothing working for me. But looking into https://filamentphp.com/docs/4.x/forms/repeater#adding-a-label-to-repeater-items-based-on-their-content table, I noticed "item" param. And it worked: ```php...
Jump to solution
17 Replies
awcodes
awcodes4w ago
Looking at the code I would expect this to work. Can you dump the state in the collapsed callback? Make sure the “done” from the records is actually cast as a Boolean.
Hache_raw
Hache_rawOP4w ago
->collapsed(function (?array $state): bool {
logger()->debug(print_r($state, true));
return $state['done'] ?? false;
})
->collapsed(function (?array $state): bool {
logger()->debug(print_r($state, true));
return $state['done'] ?? false;
})
The problem, as I expected, is that $state is an array containing ALL items. Is not like inside Table were you access to each row. This is inside Form, while Editing Order. Maybe I should had state that.
[2025-08-11 23:28:06] local.DEBUG: Array
(
[record-441] => Array
(
[id] => 441
[name] => ABRIDORES BAMBÚ
[assignee] => j
[done] => 1
[order_id] => 506
[created_at] => 2025-07-18T15:32:47.000000Z
[updated_at] => 2025-08-11T18:28:06.000000Z
)

[record-442] => Array
(
[id] => 442
[name] => MARCASITIOS CIRCULAR + IMÁN
[assignee] => j
[done] => 1
[order_id] => 506
[created_at] => 2025-07-18T15:32:47.000000Z
[updated_at] => 2025-08-11T18:28:06.000000Z
)
)

[2025-08-11 23:28:06] local.DEBUG: Array
(
[record-441] => Array
(
[id] => 441
[name] => ABRIDORES BAMBÚ
[2025-08-11 23:28:06] local.DEBUG: Array
(
[record-441] => Array
(
[id] => 441
[name] => ABRIDORES BAMBÚ
[assignee] => j
[done] => 1
[order_id] => 506
[created_at] => 2025-07-18T15:32:47.000000Z
[updated_at] => 2025-08-11T18:28:06.000000Z
)

[record-442] => Array
(
[id] => 442
[name] => MARCASITIOS CIRCULAR + IMÁN
[assignee] => j
[done] => 1
[order_id] => 506
[created_at] => 2025-07-18T15:32:47.000000Z
[updated_at] => 2025-08-11T18:28:06.000000Z
)
)

[2025-08-11 23:28:06] local.DEBUG: Array
(
[record-441] => Array
(
[id] => 441
[name] => ABRIDORES BAMBÚ
tbh I'm a bit rusted in web development. And new to Laravel ecosystem (i've been using cakephp at work for years) I'm loading that JS code from a resouce, using this in the AppServiceProvider
public function boot(): void
{
FilamentAsset::register([
Js::make('collapse-repeater', resource_path('js/collapse-repeater.js')),
]);
}
public function boot(): void
{
FilamentAsset::register([
Js::make('collapse-repeater', resource_path('js/collapse-repeater.js')),
]);
}
I don't know if that's the best way. I actually like to load it only in OrderResources and not App-wide. Anyway... Livewire listener works (i don't even know how) but after that, I can't find a good way to diferenciate elements because in the HTML they have no id attribute. They have the ID inside the cass with an UUID, etc
awcodes
awcodes4w ago
Right, but the view is checking each item’s collapsed state. Yea because the repeater’s state has UUID’s for each item in the array. But the code should still work Very well could be a conflict with persisting the collapsed state in the alpine data in local storage too, but I wouldn’t expect that to be a problem since you aren’t explicitly telling it to preserve the collapsed state. Maybe try (bool) $state[‘done’]
Hache_raw
Hache_rawOP4w ago
This plain works.
->collapsed(fn () => rand(0, 1) === 1)
->collapsed(fn () => rand(0, 1) === 1)
But as I said (may be wrong), it looks like that $state is an array of several items so I don't know how to access the correct item. If I try (bool) $state[‘done’] it throws: Undefined array key "done" because of that
[
"record-441" => [
"id" => 441
"name" => "ABRIDORES BAMBÚ"
"done" => 1
"order_id" => 506
],
"record-442" => [
"id" => 442
"name" => "MARCASITIOS CIRCULAR + IMÁN"
"done" => 1
"order_id" => 506
]
]

[
"record-441" => [
"id" => 441
"name" => "ABRIDORES BAMBÚ"
"done" => 1
"order_id" => 506
],
"record-442" => [
"id" => 442
"name" => "MARCASITIOS CIRCULAR + IMÁN"
"done" => 1
"order_id" => 506
]
]

That's $state
awcodes
awcodes4w ago
Ah, inject $arguments into the call back then you should be able to target the key in the $state array from the $arguments array. Hope I’m saying that right. isCollapased() passes the item back in. So there’s should be a way to get the corresponding key for each item in the callback.
Hache_raw
Hache_rawOP4w ago
I don't know what I'm doing 😭
->collapsed(function (?array $state, callable $get, array $arguments): bool {
$index = $arguments['index'] ?? null;

if ($index === null || !isset($state[$index])) {
return false;
}

return (bool) $state[$index]['done'];
})
->collapsed(function (?array $state, callable $get, array $arguments): bool {
$index = $arguments['index'] ?? null;

if ($index === null || !isset($state[$index])) {
return false;
}

return (bool) $state[$index]['done'];
})
ERROR: An attempt was made to evaluate a closure for [Filament\Forms\Components\Repeater], but [$arguments] was unresolvable.
awcodes
awcodes4w ago
Maybe $arguments isn’t right, but the item is passed back, maybe it $data I might be confusing actions with generic callbacks. Sorry.
Hache_raw
Hache_rawOP4w ago
Yeah, $data is also unresolvable. I don't understand the magic between Filament, Livewire and Alpine... But it looks like in the "backend" ->collapsed() can't get each database row separately So yeah, ->collapsed(fn () => rand(0, 1) === 1) works and some are collapsed in the browser but i don't think it can access each record separately
Hache_raw
Hache_rawOP4w ago
Example See? ->itemLabel()'s $state is a single record but ->collapsible()'s $state has all the records. I don't know why. I might open a Feature Request in github I guess?
Repeater::make('item')
->collapsed(function (?array $state): bool {
logger()->debug(print_r(['collapsed' => $state], true));
return (bool) $state['done'];
})
->itemLabel(function (?array $state): bool {
logger()->debug(print_r(['itemLabel' => $state], true));
return (bool) $state['name'];
}),
Repeater::make('item')
->collapsed(function (?array $state): bool {
logger()->debug(print_r(['collapsed' => $state], true));
return (bool) $state['done'];
})
->itemLabel(function (?array $state): bool {
logger()->debug(print_r(['itemLabel' => $state], true));
return (bool) $state['name'];
}),
No description
awcodes
awcodes4w ago
Since it’s a relationship I wonder if using $record in the callback gives you what your need instead of state. Repeaters are hard.
Hache_raw
Hache_rawOP4w ago
Nah, I'm editing an "Order" and it has many "items". If I use $record, I get the Order. If I use $record->items I get all the items like whem using $state so i can't access to the current one in the loop. I've opened a Feature Request (Idea) but since I'm spanish, I don't know if I expresed myself well: https://github.com/filamentphp/filament/discussions/17269
GitHub
Enable access to individual item state in ->collapsed() callback ...
I have Order and it has many associated Items and I use a Repeater to edit or add Items to an Order Currently, when creating a Form, the ->collapsed() callback on a Repeater component only recei...
Hache_raw
Hache_rawOP4w ago
btw, someone asked this back in 2022 but it seems like only the two of us wanted this functionality (:
awcodes
awcodes4w ago
Fair enough. Sorry I don’t have a solid answer for you.
Hache_raw
Hache_rawOP4w ago
No problem. I really apreciate your time 🙂
awcodes
awcodes4w ago
Just seems like the key should be available with one of the injected parameters. Item is getting passed back.
Solution
Hache_raw
Hache_raw4w ago
Hey, i looked into Laravel 4.x docs and they added more utilities. Looking at the https://filamentphp.com/docs/4.x/forms/repeater#collapsing-items table, nothing working for me. But looking into https://filamentphp.com/docs/4.x/forms/repeater#adding-a-label-to-repeater-items-based-on-their-content table, I noticed "item" param. And it worked:
->collapsed(function ($item): bool {
return $operation != "create" && $item->model->done;
})
->collapsed(function ($item): bool {
return $operation != "create" && $item->model->done;
})
PS: I had to remove some incompatible plugins to update to Filament 4 beta to try this. Then, I discarded all the changes and went back to 3.x just to confirm it also works even while not stated in the 3.x docs. So this is the current solution so far; using $item->model->ATTRIBUTE

Did you find this page helpful?