filament icon indicating copy to clipboard operation
filament copied to clipboard

Relationship still being saved even when `->dehydrated(false)`

Open ralphjsmit opened this issue 3 months ago • 6 comments

Package

filament/filament

Package Version

v4.0.12

Laravel Version

v12.28.1

Livewire Version

v3.6.4

PHP Version

8.3.25

Problem description

I am trying to convert some forms to use the new visibleJs()/hiddenJs() methods. One simple form example is a ToggleButtons field that selects an enum, and then a Select field that only is shown for one of the toggle button options.

Previously, I just used a ->live() and a ->visible(fn (Get $get) => $get('type') === ...) which worked fine, meaning that the second customer_id field would be dehydrated as null in the database. Even, in the situation that a user first selects a toggle button option that has the Select visible, the user selects a value from the Select and then the user changes the toggle button again so that the Select is hidden. In that case, even if the Select has a state selected, as it is not visible it was still dehydrated/saved as null.

Now, I converted this setup to a visibleJs(). This works ok, but I noticed that when the Select has a state and is then made hidden again, the value is still being dehydrated. That is then understandable, as the Filament backend has no way to know whether a field is hidden or visible in JS. To solve that then, I wanted to apply a ->dehydrated(fn () => false) closure, that would tell Filament to not dehydrate the field.

However, Filament still dehydrates the field. I suspect that is because of the ->relationship()-call. In my opinion, I think these relationships should respect the ->dehydrated() as well, because otherwise in the below example I do not see any way to convert this to the ->visibleJs() method.

ToggleButtons::make('type')
    ->options(DocumentType::class)
    ->inline()
    ->required(),
Select::make('customer_id')
    ->relationship('customer', 'name')
    ->searchable()
    ->preload()
    ->visibleJs(function () {
        $documentTypeCustomer = DocumentType::Customer;

        return <<<JS
            \$get('type') === '{$documentTypeCustomer->value}'
        JS;
    })
    ->dehydrated(function (Get $get) {
        $type = $get('type');

        ray($type);

        $documentTypeCustomer = DocumentType::Customer;

        $isDehydrated = $type === $documentTypeCustomer;

        ray('Dehydrated customer id: ' . $isDehydrated ? 'true' : 'false');

        return $isDehydrated;
    })
    ->requiredUnless('type', DocumentType::Customer->value),

There are two methods like ->saveRelationshipsWhenDisabled() and ->saveRelationshipsWhenHidden(). But I think I am of the opinion that a relationship should not be saved if the field is not dehydrated.

Expected behavior

See above.

Steps to reproduce

  1. Go to Documents > Create.
  2. Select the value "Customer-specific document"
  3. Select a customer
  4. Then switch back to type "Global document".
  5. Then save the document.

I expect the customer_id field to be null in the database.

Reproduction repository (issue will be closed if this is not valid)

https://github.com/ralphjsmit/demo/tree/rjs/bug-report-dehydrate

Relevant log output


ralphjsmit avatar Sep 13 '25 10:09 ralphjsmit

But I think I am of the opinion that a relationship should not be saved if the field is not dehydrated.

Fields like repeater which have a relationship are not dehydrated either, yet their relationships still need saving.

I think you need ->dehydrated(false)->saveRelationshipsUsing(null)

danharrin avatar Sep 14 '25 08:09 danharrin

I think I'll do a specific check just for select where if it is a singular select, it will not save the relationships when not dehydrated

danharrin avatar Sep 14 '25 14:09 danharrin

Uh, thinking about it more, I'm not going to do that. It would be unexpected behaviour, since saving the relationships is still a distinct process to dehydration.

Maybe we just need an easier way to handle "is this component saved or not"

danharrin avatar Sep 14 '25 14:09 danharrin

I think the BelongsTo relationship is a bit a special one as that one is acting "in essence" like any other direct model-field (though of course strictly it is still a relationship) and it not really saved by ->saveRelationshipsUsing().

If you have a method suggestion, then let me know and I make a PR.

ralphjsmit avatar Sep 16 '25 09:09 ralphjsmit

The reason we still use saveRelationshipsUsing() for BelongsTo is that some people don't have it in their $fillable array

danharrin avatar Sep 16 '25 09:09 danharrin

Another alternative I tested was to provide both a ->visible() and a ->visibleJs() simultaneously (with the idea that it would work dynamically in frontend JS and on the backend it would also be able to determine the state). However, then the frontend version was ignored iirc and only the backend version was used. It could be an option as well to consider making these supported simultaneously.

ralphjsmit avatar Sep 16 '25 09:09 ralphjsmit