Symfony UX Packages
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeHead over to https://ux.symfony.com. This is the site for the Symfony UX Initiative: a group of PHP and JavaScript packages that give us free Stimulus controllers. There's a Stimulus controller that can render chart.js, one that can add an image cropper, and so on.
Today we're going to focus on grabbing a free Stimulus controller that will give us a fancy autocomplete select
element. You can search, select - it's all very nice.
On our site, head to the voyages section and hit edit. The form has a planet dropdown, which is fine... but I want to give it more awesomeness!
Installing UX Autocomplete
So let's get this package installed. The UX Autocomplete library is a mixture of PHP with a Stimulus controller inside. Copy the composer require
line and paste:
composer require symfony/ux-autocomplete
When that finishes... run:
git status
Oooh: the recipe modified two interesting things: controllers.json
and importmap.php
We know that everything in the assets/controllers/
directory will be available as a Stimulus controller. In addition, anything in controllers.json
will also be registered as a Stimulus controller:
{ | |
"controllers": { | |
"@symfony/ux-autocomplete": { | |
"autocomplete": { | |
"enabled": true, | |
"fetch": "eager", | |
"autoimport": { | |
"tom-select/dist/css/tom-select.default.css": true, | |
"tom-select/dist/css/tom-select.bootstrap5.css": false | |
} | |
} | |
} | |
}, | |
"entrypoints": [] | |
} |
It's a way for third-party PHP packages to add more controllers. The recipe added this entry, which basically means that it'll grab some code from the package we just installed and register it as a Stimulus controller.
The point is, we now have a third Stimulus controller in our app! The other change the recipe made is in importmap.php
: it added a new entry for tom-select
:
// ... lines 1 - 15 | |
return [ | |
// ... lines 17 - 29 | |
'tom-select' => [ | |
'version' => '2.3.1', | |
], | |
]; |
tom-select
is a JavaScript package... and it's actually what does the heavy lifting for the autocomplete functionality. This stimulus controller is just a small wrapper around tom-select
. And so, since that controller needs tom-select
, it was added!
UX "autoimport" CSS
But when we refresh the page, we are greeted with a lovely error. It says
The
autoimport
tom-select.default.css
could not be found inimportmap.php
. Try runningimportmap:require
and then that path.
Look back into controllers.json
. Sometimes, these controllers have an extra section called autoimport
:
{ | |
"controllers": { | |
"@symfony/ux-autocomplete": { | |
"autocomplete": { | |
// ... lines 5 - 6 | |
"autoimport": { | |
"tom-select/dist/css/tom-select.default.css": true, | |
"tom-select/dist/css/tom-select.bootstrap5.css": false | |
} | |
} | |
} | |
}, | |
// ... line 14 | |
} |
The idea is that a Stimulus controller might have a CSS file that goes along with it. This section allows you to activate or deactivate those CSS files. For example, with tom-select
, there's a default CSS file. Or if you're using Bootstrap, you can use the Bootstrap 5 CSS file. We could set this to false
and this to true
.
One difference between using JavaScript modules in a browser versus Node & Webpack is how much of the package you get. With Node, when you npm add tom-select
, that downloads the entire package: the JavaScript files, CSS files and anything else. But with AssetMapper & the browser environment in general, when you importmap:require tom-select
, that downloads a single file: just the JavaScript file. The CSS files are not there.
However, with importmap:require
, we can, of course, grab a package with its name, like this:
php bin/console importmap:require tom-select
Cool. But we can also import a specific file path within that package. And, because AssetMapper support CSS files, that path can be to a CSS file.
In other words, if we need this vendor CSS file, we can get it with:
php bin/console importmap:require tom-select/dist/css/tom-select.default.css
Got it! Over in the assets/vendor/
directory... there it is! And in importmap.php
, it's there too. This means it's available for our Stimulus controller to import.
The end result? Error gone! And in the page source, there's the CSS file.
Applying Autocomplete to a Field
Ok, after one composer require
call, one importmap:require
call and a ton of me yapping, we have a new autocomplete Stimulus controller ready to go.
We could add a data-controller
to the select
element. But remember: UX packages are usually a mixture of Stimulus controllers and PHP code. In this case, the PHP code allows us to activate the controller directly in our form. Open up src/Form/VoyageType.php
. The planet
field is an EntityType
:
// ... lines 1 - 10 | |
class VoyageType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options): void | |
{ | |
$builder | |
// ... lines 16 - 19 | |
->add('planet', null, [ | |
'choice_label' => 'name', | |
'placeholder' => 'Choose a planet', | |
]) | |
; | |
} | |
// ... lines 26 - 32 | |
} |
And, thanks to the new package, any EntityType
or ChoiceType
now has an autocomplete
option. Set it to true
:
// ... lines 1 - 10 | |
class VoyageType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options): void | |
{ | |
$builder | |
// ... lines 16 - 19 | |
->add('planet', null, [ | |
// ... lines 21 - 22 | |
'autocomplete' => true, | |
]) | |
; | |
} | |
// ... lines 27 - 33 | |
} |
And now... Ta-da! Well, the fashion police might not love this, but it works! That option activated the Stimulus controller: you can even see it on the page. Here's the select
now with a data-controller
followed by that controller's long name.
Customizing the CSS
How can we make this look better? Thanks to the autoimport
, the tom-select.default.css
at least makes it look okay. If we were using Bootstrap, I'd change this to true
, this to false
, importmap:require
the Bootstrap file and we'd be good.
Right now, there's no official support for Tailwind, so we'll style it manually. Over in assets/styles/app.css
, I'll remove the body
. In addition to the Tailwind stuff, you can paste in whatever custom styling you need. These override some of the default styles to look nice in our space-themed, dark mode:
@tailwind base; | |
@tailwind components; | |
@tailwind utilities; | |
body { | |
background-color: skyblue; | |
} | |
/* tom-select custom styling */ | |
/* Base Styles for Dark Mode */ | |
.ts-wrapper { | |
@apply border-gray-600; | |
} | |
.ts-wrapper .ts-control, | |
.ts-wrapper.single .ts-control, | |
.ts-wrapper.single.input-active .ts-control, | |
.full .ts-control, | |
.ts-dropdown { | |
@apply bg-gray-800 text-white !important; | |
box-shadow: none ; | |
background-image: none ; | |
border: none ; | |
} | |
/* Specific Style for the Input Field */ | |
.ts-wrapper .ts-control > input, | |
.ts-wrapper.single .ts-control > input { | |
@apply bg-transparent text-white; | |
} | |
.ts-wrapper .ts-dropdown .option { | |
@apply bg-gray-800 text-white; | |
} | |
/* Active and Hover States for Dropdown Items */ | |
.ts-wrapper .ts-dropdown .active, | |
.ts-wrapper .ts-dropdown [data-selectable]:hover { | |
@apply bg-gray-700 text-white; | |
} | |
/* Disabled and Focus States */ | |
.ts-wrapper.disabled .ts-control, | |
.ts-wrapper.focus .ts-control { | |
@apply bg-gray-700 text-gray-400 border-gray-500; | |
} | |
/* Multi-select Tags Style */ | |
.ts-wrapper.multi .ts-control > div { | |
@apply bg-gray-600 text-white; | |
} | |
/* Border Radius Adjustments */ | |
.ts-wrapper .ts-control, | |
.ts-wrapper .ts-dropdown, | |
.ts-wrapper .ts-control > div { | |
@apply rounded-md; | |
} | |
/* Dropdown Box Shadow */ | |
.ts-wrapper .ts-dropdown { | |
@apply shadow-md; | |
} |
And now... love it!
Making UX Controllers Lazy
Oh, and remember how we can make our controllers lazy by adding a special comment? We can do the same thing with controllers loaded in controllers.json
by setting fetch
to lazy
:
{ | |
"controllers": { | |
"@symfony/ux-autocomplete": { | |
"autocomplete": { | |
// ... line 5 | |
"fetch": "lazy", | |
// ... lines 7 - 10 | |
} | |
} | |
}, | |
// ... line 14 | |
} |
Check it out. Go to the voyages page. I'll go to my network tools, refresh and search for "autocomplete"... and "TomSelect". Nothing! But as soon as we go to the edit page where that's being used: search for autocomplete. There it is! "TomSelect" and the CSS file were also loaded lazily, only when we needed them.
We're now done with day 8! A full week and day into LAST stack! Tomorrow, we're going to crank it up to eleven and transform our app into a sleek, single-page wonder with Turbo! Over the next 7 days... things wil start to get crazy.
Hello,
Thank you for the tutorial. I following it and encountered a problem when trying to make the autocomplete from the Type Form.
I've got the same configurations as the ones described in the video and the builder contains :
But, when I refresh the page, nothing happens.
The files tom-select.default.css and tom-select.index.js are loaded (I can see then in the network tab).
Also, if I paste the following directly in the input field via the inspector, it's working perfectly :
data-controller="symfony--ux-autocomplete--autocomplete" data-symfony--ux-autocomplete--autocomplete-max-results-value="10" data-symfony--ux-autocomplete-no-results-found-text-value="No results found" data-symfony--ux-autocomplete--autocomplete-no-more-results-text-value="No more results" data-symfony--ux-autocomplete--autocomplete-preload-value="focus"
Do you know where the problem could come from ? Thank you for your help.