Select afterStateUpdated does not fire when selection is cleared

When the select component is set to native HTML5 and you choose the placeholder option ('Select an option' option) then afterStateUpdated event raises with $status null, but if you set the select component to JavaScript and you click the x button afterStateUpdated is not fired and the placeholder option can't be selected even if you use ->selectablePlaceholder(true)
Select::make('status')
->afterStateUpdated(fn (?string $state) => dd($state))
->selectablePlaceholder(true)
->live()
->native(false)
->options([
'draft' => 'Draft',
'reviewing' => 'Reviewing',
'published' => 'Published',
]),
Select::make('status')
->afterStateUpdated(fn (?string $state) => dd($state))
->selectablePlaceholder(true)
->live()
->native(false)
->options([
'draft' => 'Draft',
'reviewing' => 'Reviewing',
'published' => 'Published',
]),
Is there any chance to catch the null option or at least when the user clicks the cross button to clear the selection?
14 Replies
krekas
krekas9mo ago
select needs to be live() then
Miguel García
Miguel García9mo ago
yeah select is set to live (see code example above), I receive the afterStateUpdate event if I choose an option, but it is not fired when i click on the cross
No description
urbycoz
urbycoz8mo ago
Did you ever figure out a way to solve this? I've got the same issue.
Miguel García
Miguel García8mo ago
Nope, sorry. I'm still having the same issue and I'm not sure whether that behaviour is on purpose or not. In my case I need to update another input's value when the cross is clicked. And I've been able to do it setting the other component to live() also.
Select::make('finished')
->afterStateUpdated(function (Set $set, ?string $state, ?string $old) {
if ($state !== '1') {
$set('finish_date', null);
}
})
->live()
->native(false)
->options([
'0' => __('shared.booleans.no'),
'1' => __('shared.booleans.yes')
])
->rules(['required']),
DatePicker::make('finish_date')
->displayFormat('d/m/Y')
->live()
->native(false)
->requiredIf('finished', '1')
Select::make('finished')
->afterStateUpdated(function (Set $set, ?string $state, ?string $old) {
if ($state !== '1') {
$set('finish_date', null);
}
})
->live()
->native(false)
->options([
'0' => __('shared.booleans.no'),
'1' => __('shared.booleans.yes')
])
->rules(['required']),
DatePicker::make('finish_date')
->displayFormat('d/m/Y')
->live()
->native(false)
->requiredIf('finished', '1')
urbycoz
urbycoz8mo ago
Ok thanks.
mariusticha
mariusticha8mo ago
using ->native(true) seems to fire afterStateUpdated i did a little research in this case and apparently the solution is slightly different. the crucial difference between your first and your second example is not that you set the second component to live (and it's also not about whether native is set to true or false, as i suggested). the problem in your first example is the dd() in afterStateUpdated. this stops the execution before the roundtrip is completely finished, leaving the app in an inconsistent state between server and client. to check if afterStateUpdated is called correctly when you deselect an option, you can use the logs. showing that everything works exactly as expected
Miguel García
Miguel García8mo ago
makes sense
mariusticha
mariusticha8mo ago
this example
Select::make('foo')
->live()
->native(false)
->afterStateUpdated(function (Set $set, ?string $state) {
$set('bar', $state);

logs()->debug('test', [
'state' => $state,
'foo' => $this->data['foo'],
'bar' => $this->data['bar'],
]);
})
->options([0, 1]),
TextInput::make('bar'),
Select::make('foo')
->live()
->native(false)
->afterStateUpdated(function (Set $set, ?string $state) {
$set('bar', $state);

logs()->debug('test', [
'state' => $state,
'foo' => $this->data['foo'],
'bar' => $this->data['bar'],
]);
})
->options([0, 1]),
TextInput::make('bar'),
brings me these results when i first select 0, then 1 and then deselect using the X:
[2023-11-16 22:44:44] laravel.DEBUG: test {"state":"0","foo":"0","bar":"0"}
[2023-11-16 22:44:46] laravel.DEBUG: test {"state":"1","foo":"1","bar":"1"}
[2023-11-16 22:44:48] laravel.DEBUG: test {"state":null,"foo":null,"bar":null}
[2023-11-16 22:44:44] laravel.DEBUG: test {"state":"0","foo":"0","bar":"0"}
[2023-11-16 22:44:46] laravel.DEBUG: test {"state":"1","foo":"1","bar":"1"}
[2023-11-16 22:44:48] laravel.DEBUG: test {"state":null,"foo":null,"bar":null}
Miguel García
Miguel García8mo ago
it turns out that if you place a date in the second select (finish_date) and then you click the cross button on the first (finished) the option in the finish_date is not set to null (on your first change) as expected unless you set it to live() so I guess I was missunderstanding it. thank you for you effort, much appreciated @mariusticha
mariusticha
mariusticha8mo ago
i even get the expected results in an example where the second input is a datepicker and its value is only set to zero if the value of the select is 1:
Select::make('foo')
->live()
->native(false)
->afterStateUpdated(function (Set $set, ?string $state) {
if ($state !== '1') {
$set('bar', null);
}

logs()->debug('test', [
'state' => $state,
'foo' => $this->data['foo'],
'bar' => $this->data['bar'],
]);
})
->options([
'0' => 0,
'1' => 1,
]),
DatePicker::make('bar')
->displayFormat('d/m/Y')
->live()
->native(false)
Select::make('foo')
->live()
->native(false)
->afterStateUpdated(function (Set $set, ?string $state) {
if ($state !== '1') {
$set('bar', null);
}

logs()->debug('test', [
'state' => $state,
'foo' => $this->data['foo'],
'bar' => $this->data['bar'],
]);
})
->options([
'0' => 0,
'1' => 1,
]),
DatePicker::make('bar')
->displayFormat('d/m/Y')
->live()
->native(false)
` bringing me these logs:
[2023-11-16 22:56:09] laravel.DEBUG: test {"state":"1","foo":"1","bar":"2023-11-30 00:00:00"}
[2023-11-16 22:56:13] laravel.DEBUG: test {"state":"0","foo":"0","bar":null}
[2023-11-16 22:56:23] laravel.DEBUG: test {"state":null,"foo":null,"bar":null}
[2023-11-16 22:56:09] laravel.DEBUG: test {"state":"1","foo":"1","bar":"2023-11-30 00:00:00"}
[2023-11-16 22:56:13] laravel.DEBUG: test {"state":"0","foo":"0","bar":null}
[2023-11-16 22:56:23] laravel.DEBUG: test {"state":null,"foo":null,"bar":null}
and everything looks as expected in the browser. imho it should be irrelevant whether the date picker is live or not thanks. it was fun to take a deeper look into this unexpected behaviour
Miguel García
Miguel García8mo ago
if you enter the page for the first time and before doing anything you place a date, then the datepicker is not set to null when you change the select you have to change the option twice in a row the first time is not set to null unless you set it (the datepicker) to live
mariusticha
mariusticha8mo ago
you are completely right. in my example above the datepicker is set to ->live(), that's why it was working 🫣
Miguel García
Miguel García8mo ago
anyway it's good to know that the events are fired as expected, makes me much more confortable mate 🙂 I should delete the dd() function from my mind 😉
AncientFriend
AncientFriend8mo ago
I think the problem here is that if you don't have a ->live() on the second input, by default you defer the update (e.g. send update on next roundtrip). Now you get ONE! update roundtrip in the backend and it evaluates all your input updates sequentially, e.g. first the input that is above the lower one. So you get the ->afterStateUpdated() from your select, it sets the state of the lower input. And after that, the update of the lower input overrides the changes done by your ->afterStateUpdated method of the upper one. If you change the order of the inputs (e.g. have the select below the input field), it should work as expected, as the Select ->afterStateUpdated() method is evaluated after the textInput state update. On the other hand, your fix with ->live() (or ->lazy()) will work as they send individual roundtrips to update the backend state. hope that helps 🙂