Custom field: bind array data

I created a custom field and this custom field renders mutliple Filament Checkboxes:
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
<!-- Interact with the `state` property in Alpine.js -->
<h1>Changing permissions for {{ $user()?->username}}</h1>
@foreach ($projectPermissions() as $permission)
<label>
<x-filament::input.checkbox x-model="state" />

<span>
{{ $permission->name }}
{{ $permission->destination->name }}
</span>
</label>
@endforeach
</div>

</x-dynamic-component>
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
<!-- Interact with the `state` property in Alpine.js -->
<h1>Changing permissions for {{ $user()?->username}}</h1>
@foreach ($projectPermissions() as $permission)
<label>
<x-filament::input.checkbox x-model="state" />

<span>
{{ $permission->name }}
{{ $permission->destination->name }}
</span>
</label>
@endforeach
</div>

</x-dynamic-component>
As you can see I bind the input to the state. The problem is that this binds every Checkbox to the state and this results in every checkbox using the same state (so only one true/false value for all checkboxes). How can I bind each checkbox to an array in the state?
39 Replies
Dennis Koch
Dennis Koch7mo ago
Add the array key to the x-model?
Nuekrato
NuekratoOP7mo ago
You mean like this: <x-filament::input.checkbox x-model="state[0]" /> ? I don't use a custom form or custom livewire component. I use the custom field inside an action form:
->actions([
Action::make('permissions')
->form([
ProjectPermissions::make('permissions')->setProject(function(RelationManager $livewire) {
return $livewire->getOwnerRecord();
})->setUser(function ($record) {
return $record;
}),
])
->action(function (array $data, $record, RelationManager $livewire) {
logger()->debug("Setting permissions: " . json_encode($data));
})
])
->actions([
Action::make('permissions')
->form([
ProjectPermissions::make('permissions')->setProject(function(RelationManager $livewire) {
return $livewire->getOwnerRecord();
})->setUser(function ($record) {
return $record;
}),
])
->action(function (array $data, $record, RelationManager $livewire) {
logger()->debug("Setting permissions: " . json_encode($data));
})
])
I treid it using this: <x-filament::input.checkbox x-model="{{state[$loop-index]}}" /> But this gives an error:
[2024-05-07 11:10:58] production.ERROR: Undefined constant "state" {"view":{"view":"/var/www/html/resources/views/forms/components/project-permissions.blade.php","data":[]},"userId":"9bb5ab66-79fe-4a37-a059-07bc95238656","exception":"[object] (Spatie\\LaravelIgnition\\Exceptions\\ViewException(code: 0): Undefined constant \"state\" at /var/www/html/resources/views/forms/components/project-permissions.blade.php:7)
[stacktrace]
#0 /var/www/html/vendor/livewire/livewire/src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php(37): include()
#1 /var/www/html/vendor/livewire/livewire/src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php(38): App\\Filament\\Resources\\ProjectResource\\RelationManagers\\UsersRelationManager->Livewire\\Mechanisms\\ExtendBlade\\{closure}()
#2 /var/www/html/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php(72): Livewire\\Mechanisms\\ExtendBlade\\ExtendedCompilerEngine->evaluatePath()
#3 /var/www/html/vendor/livewire/livewire/src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php(16): Illuminate\\View\\Engines\\CompilerEngine->get()
#4 /var/www/html/vendor/laravel/framework/src/Illuminate/View/View.php(207): Livewire\\Mechanisms\\ExtendBlade\\ExtendedCompilerEngine->get()
#5 /var/www/html/vendor/laravel/framework/src/Illuminate/View/View.php(190): Illuminate\\View\\View->getContents()
#6 /var/www/html/vendor/laravel/framework/src/Illuminate/View/View.php(159): Illuminate\\View\\View->renderContents()
#7 /var/www/html/vendor/filament/support/src/Components/ViewComponent.php(106): Illuminate\\View\\View->render()
#8 /var/www/html/vendor/laravel/framework/src/Illuminate/Support/helpers.php(117): Filament\\Support\\Components\\ViewComponent->toHtml()
#9 /var/www/html/vendor/filament/forms/resources/views/component-container.blade.php(101): e()
[2024-05-07 11:10:58] production.ERROR: Undefined constant "state" {"view":{"view":"/var/www/html/resources/views/forms/components/project-permissions.blade.php","data":[]},"userId":"9bb5ab66-79fe-4a37-a059-07bc95238656","exception":"[object] (Spatie\\LaravelIgnition\\Exceptions\\ViewException(code: 0): Undefined constant \"state\" at /var/www/html/resources/views/forms/components/project-permissions.blade.php:7)
[stacktrace]
#0 /var/www/html/vendor/livewire/livewire/src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php(37): include()
#1 /var/www/html/vendor/livewire/livewire/src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php(38): App\\Filament\\Resources\\ProjectResource\\RelationManagers\\UsersRelationManager->Livewire\\Mechanisms\\ExtendBlade\\{closure}()
#2 /var/www/html/vendor/laravel/framework/src/Illuminate/View/Engines/CompilerEngine.php(72): Livewire\\Mechanisms\\ExtendBlade\\ExtendedCompilerEngine->evaluatePath()
#3 /var/www/html/vendor/livewire/livewire/src/Mechanisms/ExtendBlade/ExtendedCompilerEngine.php(16): Illuminate\\View\\Engines\\CompilerEngine->get()
#4 /var/www/html/vendor/laravel/framework/src/Illuminate/View/View.php(207): Livewire\\Mechanisms\\ExtendBlade\\ExtendedCompilerEngine->get()
#5 /var/www/html/vendor/laravel/framework/src/Illuminate/View/View.php(190): Illuminate\\View\\View->getContents()
#6 /var/www/html/vendor/laravel/framework/src/Illuminate/View/View.php(159): Illuminate\\View\\View->renderContents()
#7 /var/www/html/vendor/filament/support/src/Components/ViewComponent.php(106): Illuminate\\View\\View->render()
#8 /var/www/html/vendor/laravel/framework/src/Illuminate/Support/helpers.php(117): Filament\\Support\\Components\\ViewComponent->toHtml()
#9 /var/www/html/vendor/filament/forms/resources/views/component-container.blade.php(101): e()
Any ideas how to use an array in the state?
Dennis Koch
Dennis Koch7mo ago
Ah, I am a bit confused because you are mixing Alpine and Blade You probably should use Alpine for the for loop. Not Blade.
Nuekrato
NuekratoOP7mo ago
Could you give me a small example?
Dennis Koch
Dennis Koch7mo ago
I think something like this:
<template x-for="(item, index) in state" :key="index">
<input x-model="state[index]">
</template>
<template x-for="(item, index) in state" :key="index">
<input x-model="state[index]">
</template>
Nuekrato
NuekratoOP7mo ago
But there are no items in the state. The permissions are not directly database fields of the model. Thats the reason why I build a custom field, because I need a custom ui and a custom handling for how the field data is handled But I will give it a try using an alpine for loop
Dennis Koch
Dennis Koch7mo ago
You would still return an array with true/false for every permission ID I guess?
Nuekrato
NuekratoOP7mo ago
Yep
Dennis Koch
Dennis Koch7mo ago
So then it's something like this?
@foreach($permissions as $permission)
<input x-model="state[{{ $permission->id }}]">
@endforeach
@foreach($permissions as $permission)
<input x-model="state[{{ $permission->id }}]">
@endforeach
Nuekrato
NuekratoOP7mo ago
Thats what I already tried (this is still a php foreach Loop?) and leads to the unknown constant error above
Dennis Koch
Dennis Koch7mo ago
No. It's what you tried but with the proper syntax.
Nuekrato
NuekratoOP7mo ago
Ah right
Dennis Koch
Dennis Koch7mo ago
Alpine-based. You mixed up JS and PHP
Nuekrato
NuekratoOP7mo ago
Yes I see. The curly braces are after the state variable. I will give it a try and let you know. Thanks!
Nuekrato
NuekratoOP7mo ago
OK. I tried it but I doesn't work as expected:
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
<!-- Interact with the `state` property in Alpine.js -->
<h1>Changing permissions for {{ $user()?->username}}</h1>
@foreach ($projectPermissions() as $permission)
<label>
<x-filament::input.checkbox x-model="state[{{$permission->id}}]" />

<span>
{{ $permission->name }}
{{ $permission->destination->name }}
</span>
</label>
@endforeach
</div>

</x-dynamic-component>
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
<!-- Interact with the `state` property in Alpine.js -->
<h1>Changing permissions for {{ $user()?->username}}</h1>
@foreach ($projectPermissions() as $permission)
<label>
<x-filament::input.checkbox x-model="state[{{$permission->id}}]" />

<span>
{{ $permission->name }}
{{ $permission->destination->name }}
</span>
</label>
@endforeach
</div>

</x-dynamic-component>
1. I get errors in the browser console:
No description
No description
Nuekrato
NuekratoOP7mo ago
2. I get an empty state after form submit: [2024-05-07 12:39:24] production.DEBUG: Setting permissions: {"permissions":null}
Dennis Koch
Dennis Koch7mo ago
It's bacause you are using UUID and those are strings not ints. So you need to wrap them inside quotes Also make sure to always pass an array as the state
Nuekrato
NuekratoOP7mo ago
I wrapped them in single quotes and the resulting HTML looks fine: state['9bb7cb42-e662-47ad-a566-d45ca240efed'] But I still get errors in the browser console (now different). In the code: <x-filament::input.checkbox x-model="state['{{$permission->id}}']" />
No description
Nuekrato
NuekratoOP7mo ago
And I don't know how to pass an array as the state. I use the custom field in the action form as stated above and I didn't find a solution to pass a state to that form.
Dennis Koch
Dennis Koch7mo ago
Not sure. I haven't build a more complex field myself. I guess you should be able to define ->default([]) for your field (if on a Create Page). Otherwise the default should already be there. It shouldn't be about the field itself because state is passed from the page.
Nuekrato
NuekratoOP7mo ago
I already had a hard time building a custom field. The docs are a bit limited regarding arrays and the data binding itself. Also it seems like there are a lot of differences in using a custom field in a form. The form itself can be used inside an action, inside a Livewire Component (e.g. custom page) or in my case: A form inside an action inside a RelationManager. Btw: Setting ->default([]) removes the errors on first opening the form with the custom field but gives errors on submitting the form. The state itself seems to be null, too [2024-05-07 13:11:04] production.DEBUG: Setting permissions: {"permissions":[]}
No description
Nuekrato
NuekratoOP7mo ago
Also what is very confusing to me is that a custom field seems to be no Livewire Component on its own and so I am unable to set properties in the custom field class and I am also unable to use $this in the custom field view, because $this references the Livewire Component the field is rendered in (in my case a RelationManager).
Dennis Koch
Dennis Koch7mo ago
Also what is very confusing to me is that a custom field seems to be no Livewire Component on its own
Yes. If every field was a Livewire component this would be a hell to maintain and probably a performance hell, too 🙈 I know custom fields are more complicated because they don't follow Livewire components.
Nuekrato
NuekratoOP7mo ago
Currently I am thinking about using a custom page instead. Is it possible to render a set of fields in a form using a foreach in Filament? For example: I need to render a Checkbox for each permission a project has - but each project can have a different amount of permissions.
Dennis Koch
Dennis Koch7mo ago
Fields are "pretty dumb". You pass in a state that can be modified and passed back.
Nuekrato
NuekratoOP7mo ago
yes... I think using a complete custom form could help me out
Dennis Koch
Dennis Koch7mo ago
Btw. what you are trying to build looks pretty similar to CheckboxList. Is there a difference?
Nuekrato
NuekratoOP7mo ago
I was trying to use CheckboxList first but I need a proper styling. I have a bit of a complicated relationship between Users, Projects, Roles and Permissions. - Users have Roles - Users have Permissions - Projects have Users through Roles - Roles and Permissions can have Polymorphic Relations to other Models (in this case a Project) - Permissions can belong to a Destination What I want to achieve: Manage the Users of a Project. But because Users are not directly related to a Project (only using a hasThrough with Roles) I did not find any built-in way in Filament to manage such a relationship. Instead I need to manage the roles and permissions of a user in the context of a project. That's the point why I thought using a custom field for the permissions could work here. The CheckboxList would be a good fit. But I want the CheckboxList to look a bit different: List the checkbox for each project permission per destination. So like: internal: [ ] Upload [ ] Download external: [ ] Upload [ ] Download and so on.
Nuekrato
NuekratoOP7mo ago
Also it seems like the CheckboxList itself can not be used in a custom field or view and is not stated in https://filamentphp.com/docs/3.x/support/blade-components/overview
Nuekrato
NuekratoOP7mo ago
Btw: Thank you so much for your time helping me out! Currently I am trying the custom page approach but I am not sure how to pass the current ownerRecord as a route parameter, because I can't access it directly using $this. Do you have an idea?
->actions([
Action::make('manage')
->url(fn (User $record): string => route('users.manage-project', $record)),
->actions([
Action::make('manage')
->url(fn (User $record): string => route('users.manage-project', $record)),
Dennis Koch
Dennis Koch7mo ago
->url(fn ($livewire) => $livewire->ownerRecord)
Nuekrato
NuekratoOP7mo ago
OK. Really nice I forgot that I can nearly use dependency injection anywhere in Filament! I am still struggling to point to my custom page.. My UserResource:
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
/* 'edit' => Pages\EditUsers::route('/{record}/edit'), */
'manage-project' => Pages\ManageProjectUser::route('/{project}/users/{record}'),
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListUsers::route('/'),
/* 'edit' => Pages\EditUsers::route('/{record}/edit'), */
'manage-project' => Pages\ManageProjectUser::route('/{project}/users/{record}'),
];
}
ProjectResource:
public static function getRelations(): array
{
return [
RelationManagers\UsersRelationManager::class,
];
}
public static function getRelations(): array
{
return [
RelationManagers\UsersRelationManager::class,
];
}
UsersRelationManager:
->actions([
Action::make('manage')
->url(fn (User $record, $livewire): string => route('users.manage-project', $record, $livewire->getOwnerRecord())),
->actions([
Action::make('manage')
->url(fn (User $record, $livewire): string => route('users.manage-project', $record, $livewire->getOwnerRecord())),
But I get the error Route [users.manage-project] not defined. I thought as soon as a page is listed in a resource I could route to it using a named route..? The route also exists:
sail artisan route:list | grep {project}
GET|HEAD admin/users/{project}/users/{record} filament.admin.resourc…
sail artisan route:list | grep {project}
GET|HEAD admin/users/{project}/users/{record} filament.admin.resourc…
Dennis Koch
Dennis Koch7mo ago
Yeah. That's not what Filament registers. Use Filaments helper method: Page::getUrl($args)
I thought as soon as a page is listed in a resource I could route to it using a named route..?
Compare the names: users. and filament.admin.resources....
Nuekrato
NuekratoOP7mo ago
ah I see I changed it to:
->actions([
Action::make('manage')
->url(fn (User $record, $livewire): string => route(ManageProjectUser::getUrl([$record, $livewire->getOwnerRecord()]))),
->actions([
Action::make('manage')
->url(fn (User $record, $livewire): string => route(ManageProjectUser::getUrl([$record, $livewire->getOwnerRecord()]))),
But this route is not defined? Route [http://localhost/admin/users/9bb5ab66-7485-4d9e-b806-a9a05abe7f91/projects/9bb7cb42-b9f8-4192-9f6c-14adf7189dbf] not defined. I am confused - exactly that route is listed in artisan route:list ?
sail artisan route:list | grep {project}
GET|HEAD admin/users/{record}/projects/{project} filament.admin.reso…
sail artisan route:list | grep {project}
GET|HEAD admin/users/{record}/projects/{project} filament.admin.reso…
Dennis Koch
Dennis Koch7mo ago
You passed an URL to the route() function. You don't need the route function anymore
Nuekrato
NuekratoOP7mo ago
You are right. I changed it:
->actions([
Action::make('manage')
->url(fn (User $record, $livewire): string => ManageProjectUser::getUrl([$record, $livewire->getOwnerRecord()])),
->actions([
Action::make('manage')
->url(fn (User $record, $livewire): string => ManageProjectUser::getUrl([$record, $livewire->getOwnerRecord()])),
But still getting a 404 error after clicking on the action button. The route in the browser looks like: http://localhost/admin/users/9bb5ab66-7485-4d9e-b806-a9a05abe7f91/projects/9bb7cb42-b9f8-4192-9f6c-14adf7189dbf Which is absolutely correct. Also user and project with those IDs exist.. The Custom Page:
<?php

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use App\Models\Project;
use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;

class ManageProjectUser extends Page
{
use InteractsWithRecord;

protected static string $resource = UserResource::class;

public ?Project $project = null;

protected static string $view = 'filament.resources.user-resource.pages.manage-project-user';

public function mount(int | string $record, string $project): void
{
$this->project = $this->resolveRecord($project);
$this->record = $this->resolveRecord($record);
}
}
<?php

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use App\Models\Project;
use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;

class ManageProjectUser extends Page
{
use InteractsWithRecord;

protected static string $resource = UserResource::class;

public ?Project $project = null;

protected static string $view = 'filament.resources.user-resource.pages.manage-project-user';

public function mount(int | string $record, string $project): void
{
$this->project = $this->resolveRecord($project);
$this->record = $this->resolveRecord($record);
}
}
Sorry - I managed to get it to work
<?php

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use App\Models\Project;
use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;

class ManageProjectUser extends Page
{
use InteractsWithRecord;

protected static string $resource = UserResource::class;

public ?Project $project = null;

protected static string $view = 'filament.resources.user-resource.pages.manage-project-user';

public function mount(int | string $record, Project $project): void
{
$this->project = $project;
$this->record = $this->resolveRecord($record);
}
}
<?php

namespace App\Filament\Resources\UserResource\Pages;

use App\Filament\Resources\UserResource;
use App\Models\Project;
use Filament\Resources\Pages\Page;
use Filament\Resources\Pages\Concerns\InteractsWithRecord;

class ManageProjectUser extends Page
{
use InteractsWithRecord;

protected static string $resource = UserResource::class;

public ?Project $project = null;

protected static string $view = 'filament.resources.user-resource.pages.manage-project-user';

public function mount(int | string $record, Project $project): void
{
$this->project = $project;
$this->record = $this->resolveRecord($record);
}
}
Seems like for the child model I can directly use Route model binding
Nuekrato
NuekratoOP7mo ago
Sorry for bothering you again but even with the custom page and a ChecbkoxList I get problems: A user has a relation directPermissions:
public function directPermissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class);
}
public function directPermissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class);
}
I use a CheckboxList on my Custom page:
<?php

namespace App\Filament\Resources\UserResource\Pages;

...

class ManageProjectUser extends Page implements HasForms
{
use InteractsWithRecord, InteractsWithForms;

protected static string $resource = UserResource::class;

public ?Project $project = null;

public ?array $data = [];

protected static string $view = 'filament.resources.user-resource.pages.manage-project-user';

public function mount(int | string $record): void
{
$this->record = $this->resolveRecord($record);
$this->form->fill();
}

public function form(Form $form): Form
{
return $form
->schema([
CheckboxList::make('directPermissions')
->relationship(
titleAttribute: 'name'
)
])
->statePath('data')
->model($this->record);
}

public function create(): void
{
dd($this->form->getState());
}
}
<?php

namespace App\Filament\Resources\UserResource\Pages;

...

class ManageProjectUser extends Page implements HasForms
{
use InteractsWithRecord, InteractsWithForms;

protected static string $resource = UserResource::class;

public ?Project $project = null;

public ?array $data = [];

protected static string $view = 'filament.resources.user-resource.pages.manage-project-user';

public function mount(int | string $record): void
{
$this->record = $this->resolveRecord($record);
$this->form->fill();
}

public function form(Form $form): Form
{
return $form
->schema([
CheckboxList::make('directPermissions')
->relationship(
titleAttribute: 'name'
)
])
->statePath('data')
->model($this->record);
}

public function create(): void
{
dd($this->form->getState());
}
}
I also see the CheckboxList in the frontend and all the permissions (see Screenshot). I thought that there is some kind of model binding and the checkboxes of those permissions the user is already attached to should be checked? I double-checked using artisan tinker that directPermissions() actually returns permissions for that user and thats the case..
No description
Nuekrato
NuekratoOP7mo ago
This is the view of the page btw:
<x-filament-panels::page>
<form wire:submit="create">
{{ $this->form }}

<button type="submit">
Submit
</button>
</form>
</x-filament-panels::page>
<x-filament-panels::page>
<form wire:submit="create">
{{ $this->form }}

<button type="submit">
Submit
</button>
</form>
</x-filament-panels::page>
Nuekrato
NuekratoOP7mo ago
I think I found a way. Also I think I found a way in using multiple CheckboxLists using a function in the form's schema as described here: https://laracasts.com/discuss/channels/filament/how-do-i-create-a-group-of-selects-with-a-foreach-loop-in-filament This way I don't need to build a custom field nor a custom form and I should be able to directly use the User's relationship. Thanks for your help!
Laracasts
Want results from more Discord servers?
Add your server