F
Filamentβ€’17mo ago
Husky110

Selfupdating / On-The-Fly / Dynamic Modal Form on Resources (Tutorial)

Hey - so this is a follow-up to #Selfupdating Modal-Form and an attempt to better state the problem and provide the found solution for future use. The problem:
Let's say you have to create an Action on a Resource (table) but the required form-entries (like which Inputs are required, which selects, or whatnot) depend on the record or a relation on the record you are working on. (I'm using the EditAction for this, but this should work on any other action as well!)
In my scenario the form-content relied on a model called "Config" and to add method to the madness, a secondary model we call "Parent". In here we call the model that the Resource displays just "Entity", to avoid confusion. So Entity belongs to Parent via parent_id. Config belongs to parent via parent_id. Entity and Config are linked via a BelongsToMany-relationship with a pivot-value called "value", which holds the set value. Now we have a Select on our form that set's the parent_id for Entity and we want the form to display all Config-models with the same parent_id as our select currently uses as selected value and update itself whenever the selected value (parent_id) changes.
The following thread will provide the solution to this, which has been found with extensive help of @pboivin and a pointer in the right direction by @smiliyas. Thank you guys. πŸ™‚
Solution:
So that's it. Just add those 3 funtions after EditAction::make() and you have yourself a selfupdating modal-form.
BUT I HAVE TO WARN YOU:
- First: This solution works nicely, but any model-policy you set on Entity is completely ignored! So those won't help you! I strongly advise to add a visible-function to your EditAction, so only users that have the appropiate authority can change anything. In my app I'm using it like this: ->visible(fn() => Auth::user()->hasRole(['Admin', 'SuperAdmin'])) - Second: Remember that everytime the Select for parent_id is changed, a request get's fired to your database. There is a theoretical attack-surface if you have a large configs-table and one does inject some JavaScript to rapidly change said select! So use this solution with causion....
Jump to solution
6 Replies
Husky110
Husky110OPβ€’17mo ago
So the first thing you have to understand is that what we are doing here is "Dependent Fields" on steroids. Some basic info can be found here: https://filamentphp.com/docs/2.x/forms/advanced#dependant-fields--components
Filament
Advanced - Form Builder - Filament
The elegant TALL stack form builder for Laravel artisans.
Husky110
Husky110OPβ€’17mo ago
Next up is that we're using 3 functions on the EditAction::make()-function.
->form(), ->mountUsing() and ->action(). Let's look at all 3 of them individually.
First up is ->form() to setup our form - the code provided even considers parent_id beeing null on Entity. The found solution looks like this:
->form(function () {
$retval = [
Forms\Components\Select::make('fulfiller_id')
->label('Parent')
->options(Parent::orderBy('name')->pluck('name', 'id'))
->placeholder('No selected Parent')
->reactive(),

];
$retval[] = Forms\Components\Group::make()->schema(function (callable $get){
$parent_id = $get('parent_id');
if($parent_id != null){
$possibleConfigs = Config::where('parent_id', $parent_id)->get();
$components = [];
foreach ($possibleConfigs as $config){
/**
Here you have to add the desired component to the $components-array.
Since I don't know how you've setup your database and such, I just provide the example for a TextInput.
**/
$components[] = TextInput::make('dynamicConfigs.'$config->id.'.value')->label($config->label);
}
return $components;
} else {
return [];
}
});
return $retval;
})
->form(function () {
$retval = [
Forms\Components\Select::make('fulfiller_id')
->label('Parent')
->options(Parent::orderBy('name')->pluck('name', 'id'))
->placeholder('No selected Parent')
->reactive(),

];
$retval[] = Forms\Components\Group::make()->schema(function (callable $get){
$parent_id = $get('parent_id');
if($parent_id != null){
$possibleConfigs = Config::where('parent_id', $parent_id)->get();
$components = [];
foreach ($possibleConfigs as $config){
/**
Here you have to add the desired component to the $components-array.
Since I don't know how you've setup your database and such, I just provide the example for a TextInput.
**/
$components[] = TextInput::make('dynamicConfigs.'$config->id.'.value')->label($config->label);
}
return $components;
} else {
return [];
}
});
return $retval;
})
We need to take a closer look on TextInput::make('dynamicConfigs.'$config->id.'.value')->label($config->label);. The 'dynamicConfigs.'$config->id.'.value'-part is actually important for the action() function later. With this notation you setup the structure needed to use Laravel's ->sync()-function. If you don't have a pivot-table and just a plain hasMany, you can use 'dynamicConfigs.'$config->id to just store anything dynamic in a dynamicConfigs-array. Now we need to look at ->mountUsing().
In our solution it looks like this:
->mountUsing(function (Forms\ComponentContainer $form, App\Entity $record){
$filling = [
'parent_id' => $record->parent_id,
'dynamicConfigs' => []
];
foreach ($record->configs as $entityConfig){
$filling['dynamicConfigs'][$entityConfig->id]['value'] = $entityConfig->pivot->value;
}
return $form->fill($filling);
})
->mountUsing(function (Forms\ComponentContainer $form, App\Entity $record){
$filling = [
'parent_id' => $record->parent_id,
'dynamicConfigs' => []
];
foreach ($record->configs as $entityConfig){
$filling['dynamicConfigs'][$entityConfig->id]['value'] = $entityConfig->pivot->value;
}
return $form->fill($filling);
})
What's happening here is that we load our Entity-model, get the configs-BelongsToMany-relationship and load the currently set pivot values. If non are set - dynamicConfigs just stays an empty array. IMPORTANT: You have to keep the variable named $record! Your Entity-instance is bound on that variablename (not sure if by Filament or Livewire). Last up is the ->action()-function. Here we grab what we have done before and store it inside the database:
->action(function ($data, $record){
if($data['parent_id'] == null){
$record->configs()->sync([]);
$record->parent_id = null;
$record->save();
} else {
$record->configs()->sync($data['dynamicConfigs']);
$record->parent_id = $data['parent_id'];
$record->save();
}
})
->action(function ($data, $record){
if($data['parent_id'] == null){
$record->configs()->sync([]);
$record->parent_id = null;
$record->save();
} else {
$record->configs()->sync($data['dynamicConfigs']);
$record->parent_id = $data['parent_id'];
$record->save();
}
})
I think that should be pretty straight forward. πŸ™‚
Solution
Husky110
Husky110β€’17mo ago
So that's it. Just add those 3 funtions after EditAction::make() and you have yourself a selfupdating modal-form.
BUT I HAVE TO WARN YOU:
- First: This solution works nicely, but any model-policy you set on Entity is completely ignored! So those won't help you! I strongly advise to add a visible-function to your EditAction, so only users that have the appropiate authority can change anything. In my app I'm using it like this: ->visible(fn() => Auth::user()->hasRole(['Admin', 'SuperAdmin'])) - Second: Remember that everytime the Select for parent_id is changed, a request get's fired to your database. There is a theoretical attack-surface if you have a large configs-table and one does inject some JavaScript to rapidly change said select! So use this solution with causion.
Saade
Saadeβ€’17mo ago
Hey @husky110, you can post this tutorial in https://filamentphp.com/tricks
Filament
Tricks - Filament
Filament is a collection of tools for rapidly building beautiful TALL stack apps, designed for humans.
Husky110
Husky110OPβ€’17mo ago
see pm πŸ™‚
Patrick Boivin
Patrick Boivinβ€’17mo ago
Thanks for the write-up @husky110, really interesting!
Want results from more Discord servers?
Add your server