F
Filament15mo ago
Vincent

Is there a good way to compose fields together in a Custom Field?

Here's what I'm trying to do: A custom field comprised of a Text Input and a Radio Input. You type a search into the text input, an API is called on the backend, and the results of the API call populate the Radio. I'd like those two inputs to be wrapped up inside a Custom Field so that I can use the default Text and Radio input style and functionality, but also add some simple custom HTML in the Blade component. Does this approach make sense, or would it be better to just copy/paste from default Filament components? Below is the (broken) code as it stands. The afterStateUpdated callback is never called, but I don't quite understand why -- I think the container call is changing the scope of the TextInput? But without setting container, I get an exception.
9 Replies
Vincent
VincentOP15mo ago
// ...
public function getTextInput(): TextInput
{
return TextInput::make($this->getId())
->label($this->getLabel())
->helperText($this->getHelperText())
->statePath($this->getQueryStatePath())
->container($this->getContainer())
->afterStateUpdated(function (?string $state, Set $set) {
$client = $this->getClient();

if (blank($state)) {
$this->datalist([]);
$set($this->statePath, '');

return;
}

$this->datalist(
collect($client->autoComplete($state))
->flatMap(fn (array $prediction) => [
$prediction['place_id'] => $prediction['description']
])
);
})
->required()
->debounce();
}

public function getRadioInput(): Radio
{
return Radio::make($this->getStatePath())
->container($this->getContainer())
->label(function (Get $get): string {
if (filled($get($this->getQueryStatePath())) && blank($this->getDatalistOptions())) {
return $this->getEmptyResultsLabel();
}

return $this->getResultsLabel();
})
->options(fn (): array => $this->getDatalistOptions() ?? [])
->hidden(fn (Get $get): bool => blank($get($this->getQueryStatePath())));
}
// ...
public function getTextInput(): TextInput
{
return TextInput::make($this->getId())
->label($this->getLabel())
->helperText($this->getHelperText())
->statePath($this->getQueryStatePath())
->container($this->getContainer())
->afterStateUpdated(function (?string $state, Set $set) {
$client = $this->getClient();

if (blank($state)) {
$this->datalist([]);
$set($this->statePath, '');

return;
}

$this->datalist(
collect($client->autoComplete($state))
->flatMap(fn (array $prediction) => [
$prediction['place_id'] => $prediction['description']
])
);
})
->required()
->debounce();
}

public function getRadioInput(): Radio
{
return Radio::make($this->getStatePath())
->container($this->getContainer())
->label(function (Get $get): string {
if (filled($get($this->getQueryStatePath())) && blank($this->getDatalistOptions())) {
return $this->getEmptyResultsLabel();
}

return $this->getResultsLabel();
})
->options(fn (): array => $this->getDatalistOptions() ?? [])
->hidden(fn (Get $get): bool => blank($get($this->getQueryStatePath())));
}
...and the view (not much HTML yet):
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
{!! $getTextInput()->toHtml() !!}
{!! $getRadioInput()->toHtml() !!}
</div>
</x-dynamic-component>
<x-dynamic-component
:component="$getFieldWrapperView()"
:field="$field"
>
<div x-data="{ state: $wire.$entangle('{{ $getStatePath() }}') }">
{!! $getTextInput()->toHtml() !!}
{!! $getRadioInput()->toHtml() !!}
</div>
</x-dynamic-component>
Using getChildComponents seems to work, but then I'm limited in the amount of custom HTML I can write. I literally just want to wrap the Radio inputs directly below the Text input.
awcodes
awcodes15mo ago
AfterStateUpdated will never get called without make the field ->live()
Vincent
VincentOP15mo ago
Hmm...this doesn't seem to make a difference, whether I add it to the parent or the child. The only thing that seems to work if I'm doing this as a custom field with children is to explicitly set the inputs as children via getChildComponents -- but that means I lose the ability to control each input individually in the view.
awcodes
awcodes15mo ago
Something is off then. Live() is what makes the afterStateUpdated run. With $set in the callbacks you can change the values of other fields in the form. And I get that you are trying to reuse as much as you can to be consistent from a ui standpoint, but when you start talking about custom components there’s going to be a point where you have to recreate it yourself.
Vincent
VincentOP15mo ago
@awcodes Thanks for the input. I get the impression I'm fighting the framework pretty hard! Other plugins seem to re-create Filament markup pretty liberally, so I guess that's what I'll do too.
awcodes
awcodes15mo ago
Not saying it can’t be done. But that’s the beauty of filament is that you are free to do what you need, when it doesn’t fit the use case. It’s still just laravel and livewire. It mostly boils down to understanding the lifecycle. And the best place to actually figure that out for your needs is to source dive. Docs can’t rally cover that in a practical way.
Vincent
VincentOP15mo ago
Yeah the new Laracasts course really helped me wrap my head around it! It's using some pretty cool PHP trickery under the hood. It feels like it must be possible, but I'm under a deadline and done is better than perfect. Gonna sleep on it & see where I land tomorrow. Meantime, I'll mark this as solved, but hopefully somebody else can chime in as well. Thanks again!
Vincent
VincentOP15mo ago
After a consulting call with @Zep Fietje (well worth the money -- thanks for the help!), I ended up using a searchable Select component from Filament. It did what I tried to cobble together above, but with a better experience overall. If I need to customize the input template more in the future, I'll have to remember that the backend/Livewrie functionality is handled by the parent's Livewire component, not the input itself. So instead of e.g. instantiating a TextInput in the form's context, I'd just use the x-filament::input component directly in my custom field's template, and have it call Livewire functionality by adding the Livewire methods to the parent component (Page, Form, etc.). (Filament's Select input is a great example of how this might be done.)
Zep Fietje
Zep Fietje15mo ago
Great summary, Vincent! 😄
Want results from more Discord servers?
Add your server