filamentphp / filament

A collection of beautiful full-stack components for Laravel. The perfect starting point for your next app. Using Livewire, Alpine.js and Tailwind CSS.
https://filamentphp.com
MIT License
18.86k stars 2.93k forks source link

Searchable field inside Repeater Closure fields causes JS errors #3575

Closed Muffinman closed 2 years ago

Muffinman commented 2 years ago

Package

filament/forms

Package Version

2.15.19

Laravel Version

9.20

Livewire Version

No response

PHP Version

8.1.9

Problem description

When adding a searchable() input inside a schema() callback \Closure, it can cause javascript errors and some general weird behaviour.

On the JS console we see errors like:

Livewire Entangle Error: Livewire property 'data.conditions.51d5b7cb-885a-4100-978a-e65c90f18fbf.customer_id' cannot be found

It also seems to fail to send the field state back for these searchable() fields when saving. Causing validation error:

Screenshot 2022-08-19 at 15 47 13

I'm using a closure to work out the correct configuration fields to render, depending on the user's selection of type. I don't want to render ALL the condition fields by default and use hidden() because we will end up with many condition types here and that could get slow / messy.

I want this config to be later JSON encoded and stored in the configuration field of the Condition model. But that part isn't important to this bug report so has been removed from the example.

Perhaps there's some other way to achieve what I need?

Expected behavior

No JS errors. State data returned in request upon save.

Steps to reproduce

Available in the example Git repo: https://github.com/Muffinman/filament-repeater-bug/blob/master/app/Filament/Resources/DiscountResource.php

  1. Create a Resource with a Repeater referencing a relationship. In my case I have a Discount model referencing one or more Conditions.
  2. Add the condition fields inside the Repeater
  3. My conditions have 'configuration' fields that needs to be added. E.g. for a Customer ID condition, we need to provide the customer.
  4. Add \Closure to Fieldset::schema() to dynamically render different fields depending on the Discount type
  5. Use the form, add an item to the repeater
  6. See console for JS error

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('title')
                ->required(),
            Forms\Components\Select::make('type')
                ->options(['fixed' => 'Fixed amount off', 'percent' => 'Percentage off'])
                ->required(),
            Forms\Components\TextInput::make('value')
                ->required(),
            Forms\Components\Repeater::make('conditions')
                ->relationship('conditions')
                ->schema([
                    Forms\Components\TextInput::make('title')
                        ->required(),
                    Forms\Components\Select::make('type')
                        ->options(['user_id' => 'User ID', 'customer_id' => 'Customer ID'])
                        ->reactive()
                        ->required(),
                    Forms\Components\Fieldset::make('configuration')
                        ->schema(function (\Closure $get) { // <- This seems to cause the issue
                            $fields = [];

                            if ($get('type') === 'customer_id') {
                                $fields[] = Select::make('customer_id')
                                    ->label('Customer')
                                    ->searchable()
                                    ->getSearchResultsUsing(fn(string $search) => Customer::where('title', 'like', "%{$search}%")->limit(50)->pluck('title', 'id'))
                                    ->getOptionLabelUsing(fn($value): ?string => Customer::find($value)?->title)
                                    ->required();
                            }

                            if ($get('type') === 'user_id') {
                                $fields[] = Select::make('user_id')
                                    ->label('User')
                                    ->searchable()
                                    ->getSearchResultsUsing(fn(string $search) => User::where('name', 'like', "%{$search}%")->limit(50)->pluck('name', 'id'))
                                    ->getOptionLabelUsing(fn($value): ?string => User::find($value)?->name)
                                    ->required();
                            }

                            $fields[] = Toggle::make('negate');
                            return $fields;
                        }),
                ])
                ->columnSpan(2)
                ->createItemButtonLabel('Add condition'),
        ]);
}

Reproduction repository

https://github.com/Muffinman/filament-repeater-bug

Relevant log output

n/a
danharrin commented 2 years ago

This is because there is no Livewire property associated with these select fields when the form is first loaded, since there is no $type selected.

So you must add some extra code to the type field to add these properties to the form state, so they aren't missing:

Forms\Components\Select::make('type')
    ->options(['user_id' => 'User ID', 'customer_id' => 'Customer ID'])
    ->reactive()
    ->required()
    ->afterStateUpdated(fn ($set) {
        $set('customer_id', null);
        $set('user_id', null);
    }),

This ensures that at all times when each select is visible, there is a Livewire property associated with the field. So the console errors will not be present.

Muffinman commented 2 years ago

Thanks so much. That didn't seem to change anything for me, but I think I understand what you're getting at - I'll keep looking!