Bonus: More on Flowbite
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 SubscribeA bonus topic! Yeah, because I started to get questions - good questions - about Flowbite. On day 5 we added Tailwind and I introduced Flowbite as a site where you can copy and paste visual components. For example, you copy this markup, paste, and boom! You have a dropdown. The classes are all standard Tailwind classes.
And so, I mentioned that you don't need to install anything. However, depending on what you want, that's not the full story... and I confused people. So let's fix that!
Installing The Flowbite JavaScript
Beyond being a source to copy HTML, Flowbite itself has two other features. First, it has an optional JavaScript library for powering things like tabs and dropdowns: a little JavaScript so that when we click, this opens and closes.
We're not using this at SymfonyCasts... and it doesn't play well with Turbo. At least not out of the box. We prefer to create tiny Stimulus controllers to power things like this. But, we can get the Flowbite JavaScript to work.
Grab that dropdown markup and zip over to templates/base.html.twig. Just inside the body, paste:
| <html> | |
| // ... lines 3 - 17 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 19 - 24 | |
| <!-- Dropdown menu --> | |
| <div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700"> | |
| <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton"> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Dashboard</a> | |
| </li> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Settings</a> | |
| </li> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Earnings</a> | |
| </li> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sign out</a> | |
| </li> | |
| </ul> | |
| </div> | |
| // ... lines 42 - 120 | |
| </body> | |
| </html> |
If we go over and refresh, you can see what I mean: it just works. Well, visually. But if we click, nothing happens.
To get the Flowbite JavaScript, find your terminal and run:
php bin/console importmap:require flowbite
This installs flowbite and it dependency @popperjs/core. It also grabbed the Flowbite CSS file... which is only needed if you didn't have Tailwind properly installed. Having it hanging around in importmap.php is harmless, but let's kick it out before it confuses me.
To use the JavaScript, open assets/app.js. On top import 'flowbite':
| // ... lines 1 - 5 | |
| import 'flowbite'; | |
| // ... lines 7 - 43 |
Ok, refresh and... it works!
But there are two... quirks. Check out the console. We have a bunch of errors about modal and popover. If you use the modal component from Flowbite, it requires a data-modal-target attribute to connect the button to the target. The problem is that we have a modal Stimulus controller.... and we're using data-modal-target to leverage a Stimulus target. Those two ideas are colliding. You would need to work around this by using Flowbite's modal system or renaming your modal controller to something else. The same is true for Popover.
Fixing Flowbite JS & Turbo
The second quirk is that, though the Flowbite JavaScript works right now, as soon as we navigate, it breaks! Flowbite initializes the event listener on page load, but when we navigate and new HTML is loaded onto the page, it's not smart enough to reinitialize that JavaScript. That's why, in general, we write our JavaScript using Stimulus controllers.
Flowbite does ship with a version of itself for Turbo... but it doesn't quite work: it doesn't reinitialize correctly on form submits.
That's ok! We've got the skills to patch this up ourselves. Import initFlowbite from flowbite:
| // ... lines 1 - 5 | |
| import { initFlowbite } from 'flowbite'; | |
| // ... lines 7 - 50 |
Then at the bottom, I'll paste in two event listeners:
| // ... lines 1 - 43 | |
| document.addEventListener('turbo:render', () => { | |
| initFlowbite(); | |
| }); | |
| document.addEventListener('turbo:frame-render', () => { | |
| initFlowbite(); | |
| }); |
Flowbite handles initializing on the first page load. Then anytime we navigate with Turbo, this method will be called and will reinitialize the listeners. Or if we do something inside a Turbo frame, this will be called.
Let's try it. Refresh. And... it doesn't work: Look: initFlobite. Typo! Fix that then... ok. On page load, it works. And if we navigate, it still works.
The Flowbite Tailwind Plugin
So the first installable feature of Flowbite is this JavaScript library. The second is a Tailwind plugin. It adds extra styles if you use tooltips, forms, and charts.... as well as a few other things. You can find the package on npmjs.com and navigate its files to find the plugin: plugin.js.
If you're using tooltips, it adds new styles, same thing for forms... then all the way at the bottom, it tweaks some theme styles. This isn't necessarily something that you need, even if you're using some of the JavaScript from Flowbite.
But if you do want this plugin, you need to install it with npm. So far, we haven't had to do anything with npm... and that's been great! But if you do need a few JavaScript libraries, that's ok: that's npm's job. The most important thing is that we don't have a giant build system: we're just grabbing a library here or there that we need.
Find your terminal and run npm init to create a package.json file.
npm init
I'll hit Enter for all the questions. Then run:
npm add flowbite
To use this, open tailwind.config.js... here it is. Down in the plugins section, require('flowbite/plugin'):
| // ... lines 1 - 3 | |
| module.exports = { | |
| // ... lines 5 - 28 | |
| plugins: [ | |
| require('flowbite/plugin'), | |
| // ... lines 31 - 34 | |
| ], | |
| } |
This is straight from their docs.
Whe we refresh, it works... but we don't see any difference. Like I said, it's not something that we necessarily need. Though if you open a form, huh: our labels are suddenly black! That's because Tailwind now thinks we're in light mode... and I was a bit too lazy to style my site for light mode.
By default, Tailwind reads whether you want light mode or dark mode from your operating system preferences. But Flowbite overrides that and changes it to read a class on your body element. It has documentation on their site on how you can use this and even make a dark mode, light mode switcher.
But I'm going to change this back to the old setting. Say darkMode, media:
| // ... lines 1 - 3 | |
| module.exports = { | |
| // ... lines 5 - 10 | |
| darkMode: 'media', | |
| // ... lines 12 - 36 | |
| } |
Check it: refresh and... we're back to normal! So that's the Tailwind plugin.
The Datepicker
In addition to these 2 Flowbite features, I've also seen people wanting to use their cool datepicker plugin. So let's get that working!
This datepicker is part of the main flowbite library. But if you want to import it directly from JavaScript... then, down here, you're supposed to install a different package. This confused me to be honest. But copy that, spin over and run:
php bin/console importmap:require flowbite-datepicker
Back at the top of the docs, it says that you can use the datepicker simply by taking an input and giving it a datepicker attribute. And that's true... except once again, it won't work with Turbo. It'll work at first... but stop after the first click.
Instead, we're going to initialize this with a Stimulus controller, and it's going to work great!
In assets/controllers/, create a new datepicker_controller.js. I'll paste in the contents:
| import { Controller } from '@hotwired/stimulus'; | |
| import { Datepicker } from 'flowbite-datepicker'; | |
| /* stimulusFetch: 'lazy' */ | |
| export default class extends Controller { | |
| datepicker; | |
| connect() { | |
| this.element.type = 'text'; | |
| this.datepicker = new Datepicker(this.element, { | |
| format: 'yyyy-mm-dd', | |
| autohide: true, | |
| }); | |
| } | |
| disconnect() { | |
| if (this.datepicker) { | |
| this.datepicker.destroy(); | |
| } | |
| this.element.type = 'date'; | |
| } | |
| } | |
| // ... lines 24 - 25 |
We're going to attach this controller to an input element. In connect(), this initializes the date picker and passes this.element. The format matches the default format that the Symfony DateType uses. And autohide makes the date picker close when you choose a date, which I like.
I'm also changing the type attribute on the input to text so that we don't have both the datepicker from Flowbite and the native browser date picker. In disconnect(), we do some cleanup.
We're going to use this on the voyage form: for "Leave at". Open the form type for this: VoyageType. Here's the field. Pass an attr option with data-controller set to datepicker:
| // ... lines 1 - 14 | |
| class VoyageType extends AbstractType | |
| { | |
| public function buildForm(FormBuilderInterface $builder, array $options): void | |
| { | |
| // ... line 19 | |
| $builder | |
| // ... line 21 | |
| ->add('leaveAt', DateType::class, [ | |
| // ... line 23 | |
| 'attr' => [ | |
| 'data-controller' => 'datepicker', | |
| ] | |
| ]) | |
| // ... lines 28 - 44 | |
| ; | |
| } | |
| // ... lines 47 - 53 | |
| } |
Let's try this! Refresh and... that's fantastic!
Fixing the Datepicker in a Modal
Though... there's a catch. Go back and open this form in the modal. It doesn't work! Well, it kind of does. See it? It's hiding behind the modal. The datepicker works by appending HTML at the bottom of the body. But because that's not inside the dialog, it correctly appears behind the modal. It's kind of a shame that it doesn't work better with the beautiful native dialog element, but we can fix this.
In datepicker_controller.js, add a new option called container. This tells the datepicker which element it should add its custom HTML into. Say document.querySelector() and look for a dialog[open]. So if there's a dialog on the page that's open, then use that as the container. Else use the normal body:
| // ... lines 1 - 4 | |
| export default class extends Controller { | |
| // ... lines 6 - 7 | |
| connect() { | |
| // ... lines 9 - 10 | |
| this.datepicker = new Datepicker(this.element, { | |
| // ... lines 12 - 13 | |
| container: document.querySelector('dialog[open]') ? 'dialog[open]' : 'body' | |
| }); | |
| } | |
| // ... lines 17 - 24 | |
| } | |
| // ... lines 26 - 27 |
Making the Modal Click Outside Smarter
And that little detail takes care of our problem! Though... it does expose one other small issue. See how the datepicker extends the dialog vertically? If we click here, we're technically clicking on the dialog element directly... which triggers our click outside logic.
To fix that, let's make our modal controller just a bit smarter. At the bottom, I'll paste in a new private method called isClickInElement():
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 65 | |
| #isClickInElement(event, element) { | |
| const rect = element.getBoundingClientRect(); | |
| return ( | |
| rect.top <= event.clientY && | |
| event.clientY <= rect.top + rect.height && | |
| rect.left <= event.clientX && | |
| event.clientX <= rect.left + rect.width | |
| ); | |
| } | |
| } |
If you pass this a click event, it will look at the physical dimensions of this element and see if the click was inside.
Up here in clickOutside(), let's change things. Copy this, then if the event.target is not the dialog, we're definitely not clicking outside. So, return.
And if not, this.isClickInElement() - passing event and this.dialogTarget - so if we did not click inside the dialogTarget - then we definitely want to close:
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 46 | |
| clickOutside(event) { | |
| if (event.target !== this.dialogTarget) { | |
| return; | |
| } | |
| if (!this.#isClickInElement(event, this.dialogTarget)) { | |
| this.dialogTarget.close(); | |
| } | |
| } | |
| // ... lines 56 - 74 | |
| } |
A bit more logic, but a bit smarter. Try it. Open the modal and if we click down here... the calendar closes - which is correct - but the modal stays open. Love that!
So I hope that explains Flowbite a bit more. Personally, I don't want most of this stuff, so I'm going to remove it. Inside tailwind.config.js, remove the plugin:
| // ... lines 1 - 3 | |
| module.exports = { | |
| // ... lines 5 - 29 | |
| plugins: [ | |
| require('flowbite/plugin'), | |
| // ... lines 32 - 35 | |
| ], | |
| } |
Then delete package.json and package-lock.json.
I also don't want the JavaScript. In importmap.php, remove flowbite and @popperjs/core:
| // ... lines 1 - 15 | |
| return [ | |
| // ... lines 17 - 51 | |
| 'flowbite' => [ | |
| 'version' => '2.2.1', | |
| ], | |
| '@popperjs/core' => [ | |
| 'version' => '2.11.8', | |
| ], | |
| // ... lines 58 - 60 | |
| ]; |
But that datepicker is cool, so let's keep that.
In app.js, remove the import from flowbite and the two functions at the bottom:
| // ... lines 1 - 5 | |
| import { initFlowbite } from 'flowbite'; | |
| // ... lines 7 - 43 | |
| document.addEventListener('turbo:render', () => { | |
| initFlowbite(); | |
| }); | |
| document.addEventListener('turbo:frame-render', () => { | |
| initFlowbite(); | |
| }); |
Finally, in base.html.twig, get rid of that random dropdown:
| <html> | |
| // ... lines 3 - 17 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 19 - 24 | |
| <!-- Dropdown menu --> | |
| <div id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700"> | |
| <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDefaultButton"> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Dashboard</a> | |
| </li> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Settings</a> | |
| </li> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Earnings</a> | |
| </li> | |
| <li> | |
| <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Sign out</a> | |
| </li> | |
| </ul> | |
| </div> | |
| // ... lines 42 - 120 | |
| </body> | |
| </html> |
Now... no more JavaScript errors! But because that datepicker was pretty cool, we still have it.
Ok, bonus chapter done! Now back to work - seeya later!
18 Comments
Hello!
I get this error when I try to run php bin/console importmap:require flowbite
Do you know why?
Hey @Octavio-B ,
Hm, it might be just a network issue or a temporary outage from jsDelivr. Or if you use a VPN or have some firewall set up - it may cause issues too. Could you try again after some time? Also, when you it this problem - try to load that URL directly in your browser, do you see a JSON output or is it not loaded as well?
Cheers!
Hello Victor,
Thank you for your quick response. You are right, it is a network issue; because when I try to load the URL directly in my browser, I don't get to see any JSON. I think it has to do with the firewall in my office. Is there another way to make this work?
Hey @Octavio-B ,
Hm, other than tweaking the firewall to allow it? Not sure, probably using a VPN may help, depends on the firewall setup, I think. Or use another CDN for this, probably the issue is only with this jsDelivr.
Cheers!
Hello everyone, I'm a French speaker and I use deepl to translate into English.
I need help with Symfony -UX-Autocomplete for a project I'm currently developing. I'm doing this in a stimulus controller that I've named test_controller.js
In my symfony form type, i have this:
But nothing happens in the browser console when I run the code and open dropdown.
I make it clear that the test_controller.js controller does work, I mean, is attached to my asset form field. When my page loads, the console tells me that the controller is active on my element. However, the autocomplete:open event, which should be activated when the Dropdown opens, doesn't work. Also, I've looked through all the available documentation about implementing @symfony/ux-autocomplete in a Symfony project, but haven't found anything about ux-autocomplete events.
Thanks for help :) !
Hey @Diarill!
Where did you get the event
autocomplete:openfrom? I don't believe that exists. I don't think UX autocomplete dispatches any event on open. However, the underlying autocomplete library "TomSelect" does dispatch some events: https://tom-select.js.org/docs/events/The implementation would look something like this (referencing https://symfony.com/bundles/ux-autocomplete/current/index.html#extending-tom-select)
Let me know if this helps!
Somehow I am not getting the form to autovalidate when the date changes with Flowbite's datepicker.
I have a date field with a NotNull constraint. The issue is noticeable when the date field is blank, the form is submitted and the not null validation error is displayed for the date field. Then selecting a date with the datepicker does not trigger the autovalidation.
It looks like the Flowbite's datepicker might be preventing the change event. The component does have a changeDate event, but I wouldn't know how to manually trigger LiveComponent's validation.
Hey @apphancer!
Hmm, I hadn't tried this yet, but it does make sense. Usually, when you have a "widget" like this, when you select a date it does, of course, update the underlying
inputwith the value. But it almost always does it in a way that does not trigger thechangeevent (iirc you need to do MORE work to have that happen). So you've diagnosed this perfectly.You can find the fix & info here - https://symfony.com/bundles/ux-live-component/current/index.html#model-updates-don-t-work-when-external-javascript-changes-a-field which really points down to here - https://symfony.com/bundles/ux-live-component/current/index.html#javascript-manual-element-change
Let me know if that helps!
Cheers!
Thanks, Ryan, for pointing me in the right direction. That was very helpful!
I think I've managed to get to the bottom of it, and.... it looks like the issue is more serious than I first imagined, and.... the datepicker in Space Inviters is affected by the same issue.
Not only the changes to this field are not submitted for validation, but if you select a date for a Voyage (i.e. a different date than the default one), then change anything in the other fields, the autovalidation triggered on the other fields resets the value selected in the date field.
Here's how I changed the code inside the connect method of the datepicker_controller:
Hey @apphancer!
Nice work! Sorry for the VERY slow reply. This is exactly the reason why we need to share Stimulus controllers as a community, so we can get little details like this correct. That's something we're working on right now for ux.symfony.com.
Cheers!
Ryan,
I may be having the same issue. I'm using LiveComponents to search through items with a Live Search, and I'm using Tailwind CSS Number Input by Flowbite, which requires Flowbite.js. The number input works, but if I search and the LiveComponent renders with the Items matched in the search, the number input still works, but if I clear the search, the number input no longer works.
I checked this:
https://symfony.com/bundles/ux-live-component/current/index.html#javascript-manual-element-change
So I've created a Stimulus controller:
And then I've added this to my component:
Is there something I'm missing?
Hey @Brandon!
Flowbite is great and a pain :).
Can you verify if:
initializemethod of your controller is being called?render:finishedcallback being executed?I'm not sure where the problem is, but that's where I would start looking. I bet other people would be interested in the solution if we can find it.
Cheers!
Ryan,
Thank you for the response. This what I've come up with
Initial page load, before any search takes place, in my console I get:
application #starting
render #initialize
render #connect
live #initialize
live #connect
application #start
I run a search which I know will return 1 result and I added a console.log('Render Finished?'); after initFlowbite();, and in my console for whatever reason it comes up three times.
Render Finished?
Render Finished?
Render Finished?
If I clear the search and return everything, it says 'Render Finished' once more and my number field works on the first 'item', but not on the other four, unless I do a full page refresh, and it works the same all over again. I don't know a lot about JavaScript, but if you suggest more to test I can do it to figure this out. I can say that there is an event on the first number input that works, but not on any of the others.
What I did to get around this is like you have suggested in the past, make a small Stimulus controller to handle the increment and decrement of the input, basically taking the place of Flowbite. It would be nice for Flowbite to work out of the box, but if I'm making a Stimulus controller to help re-render Flowbite, I might as well use the controller to work right out the box with LiveComponents and increment and decrement my quantity.
Thank you for all your help, I wish you the best
Hey @Brandon!
Nice job! And this really summarizes my feelings about the Flowbite JavaScript: it's super enticing, but the implementation is not what I want. The initialization code is really hardwired to depend on full page refreshes and definitely doesn't play well with the DOM mutation that LiveComponents uses (and that more and more things use, like Turbo 8 morphing). Ideally, as morphing becomes more common, libraries like Flowbite will work better with them. But until that fancy future, Flowbite offers these wonderful things that are beautiful and instant... until they stop working. Then you've gotta debug and it may or may not feel worth it in the end, which is a shame.
So good work, and I wish I had a magical solution, because I also want to use Flowbite more often :)
Cheers!
Thanks so much for this course, and really, all of them. The presentation style keeps me engaged and the videos are just the right length.
Six months ago I came back to PHP/Symfony and had to update a PHP 5.x / Symfony 2.x project to PHP 8 / Symfony 6.3 and this site has been invaluable for getting up to speed quickly. So much has advanced in the last 8 years since I lived it back in 2013 - 2015. Upgrading wasn't really an option so I ended up using the old code as a guide and just re-wrote the whole thing. Lines and lines of custom JS just vanished once I got using Stimulus and it was so much easier. It did remind me of how I have a love/hate relationship with Bootstrap. Then I watched the Tailwind videos here and just started using it this week and I love having all my style definitions right where I can see them. Also switched from Webpack to AssetMapper... so much easier.
Hey @Shay-H
Thanks for you kind words, we're here to help :)
Cheers!
Hey Ryan. Thanks for providing this bonus video.
To fix the form styles for dark mode/light mode you changed
darkModetomedia. I assume this is working for you because you use dark mode on your operating system, or at least your browser. But if someone following the lesson uses light mode in their browser, wouldn't the issue still remain? I think a better solution would be to addclass="dark"to the<html>element of the base template, since the entire app uses a dark theme and does not support toggling themes. I suggest adding that class to the<html>element only because that is what the flowbite docs suggest.Also, it's cool that you can opt to just use the flowbite-datepicker js and not the rest since that would be the one flowbite component with interactivity that would be the most painful to rebuild from scratch.
Hey @jdevine!
Yes - I get your point. I guess there are two ways of thinking about it:
1) I was imagining that, in a real app, you would be less lazy than I was and actually code the templates to properly support light mode. If you did that, then the auto-detection is perfectly great.
2) But, if we are determined to have this be a space-themed dark site only, then totally: we should use the
classand setclass="dark"like you suggested :).Ah! I'm really glad you thought so! I had the same thinking: all of the other JS (tabs, dropdowns, even the modal) seem relatively simple. A rich date picker on the other hand... yea, that's much bigger.
Cheers!
"Houston: no signs of life"
Start the conversation!