Tricks

Render HTML in select options

Jul 12, 2022
Matthew Ost
Form builder

Sometimes a single attribute just isn't enough to identify the correct record in a select fields options. Sure, you can concatenate attributes in an accessor but it can be ugly. What if you need to display an image? Sounds like we need a little HTML!

Since Filament implemented Choices.js we've had the ability to render HTML inside each select option. You just need to switch the feature on using the new allowHtml() modifier when defining your select field.

Here's a common example of a Select field for users with avatars.

Select::make('user_id')
->label('User')
->allowHtml() // Apply the new modifier to enable HTML in the options - it's disabled by default
->searchable() // Don't forget to make it searchable otherwise there is no choices.js magic!
->getSearchResultsUsing(function (string $search) {
$users = User::where('name', 'like', "%{$search}%")->limit(50)->get();
 
return $users->mapWithKeys(function ($user) {
return [$user->getKey() => static::getCleanOptionString($user)];
})->toArray();
})
->getOptionLabelUsing(function ($value): string {
$user = User::find($value);
 
return static::getCleanOptionString($user);
})

->getSearchResultsUsing() - needs to return a key/value pair for each search result. The key should be the model (User) id and the value should be your HTML string.

->getOptionLabelUsing() - needs to return your HTML string.

To keep it DRY I've added a static method to the resource to return the view.

public static function getCleanOptionString(Model $model): string
{
return Purify::clean(
view('filament.components.select-user-result')
->with('name', $model?->name)
->with('email', $model?->email)
->with('image', $model?->image)
->render()
);
}

Note: enabling allowHtml() can introduce vulnerability to XSS attacks. Sanitize your HTML strings to stay safe!

Finally, create a blade file for the view

<div class="flex rounded-md relative">
<div class="flex">
<div class="px-2 py-3">
<div class="h-10 w-10">
<img src="{{ url('/storage/'.$image.'') }}" alt="{{ $name }}" role="img" class="h-full w-full rounded-full overflow-hidden shadow object-cover" />
</div>
</div>
 
<div class="flex flex-col justify-center pl-3 py-2">
<p class="text-sm font-bold pb-1">{{ $name }}</p>
<div class="flex flex-col items-start">
<p class="text-xs leading-5">{{ $email }}</p>
</div>
</div>
</div>
</div>
avatar

How can i use livewire component like "blade-icon" in view? if i use Purify ( https://github.com/stevebauman/purify) the package remove the component :S

Congrats, great example!

avatar
Guilherme Haynes Howe

I solved it by removing Purify

avatar
Guilherme Haynes Howe

How can I use preload with this custom component?

avatar
->options(function () {
//return options you want to preload
})
 
```
avatar

Hi ! Nice one, but it is possible to have this renderer for the defaut option visible before doing a search ?

avatar

just use the same mechanism for options() works like a charm

->options(function (string $search) {
$users = User::All();
 
return $users->mapWithKeys(function ($user) {
return [$user->getKey() => static::getCleanOptionString($user)];
})->toArray();
})
avatar

Amazing !

avatar

what if user want only name to be selected and in options it will show both name and email>

avatar

I can't get any of the markup working. I have the allowHtml but the select list is just rendered in plain formatting.

avatar

you have to add getOptionLabelUsing and getOptionLabelsUsing

->getOptionLabelsUsing(function ($value): string {
$character = Auth::user()->characters()->find($value);
return static::getCharacterRenderer($character);
})
->getOptionLabelUsing(function ($value): string {
$character = Auth::user()->characters()->find($value);
return static::getCharacterRenderer($character);
}),
avatar

Amazing work ! Thank you so much !

avatar

you're welcome !

avatar

what if user want only name to be selected and in options it will show both name and email?

avatar

Perfect solution! But if I use select component with allowHtml() then reactive disabled(fn () ..) is not working :(

avatar

THANKS, I used this on Filament3, it works like a charm - though I need to refactor and claen my code.

I am now looking to do similar with FILTERs, any ideas ?

avatar

Would this introduce an N+1?

avatar

Amazing work ! Thank you so much ! But how I can implement similar with relationship? e.g. ->relationship('product','name') ....