flowbite
flowbite copied to clipboard
Flowbite with Turbo on rails7
Hi! 👋
Firstly, thanks for your work on this project! 🙂
I got Flowbite working together with Turbo in my Rails 7 project. The original issue was that the javascript worked until the first turbo navigation was made and stoped working after that.
According to Turbo's documentaction this happened because Flowbite uses DOMContentLoaded
event to install JS behavior, but this event only fires once, in response to the initial page load.
In order to install the JS behavior on every page change I replaced DOMContentLoaded
to turbo:load
event in dist/flowbite.js
. turbo:load
event fires on every page change.
I used patch-package to patch [email protected]
for the project I'm working on.
Here is the diff that solved my problem:
diff --git a/node_modules/flowbite/dist/flowbite.js b/node_modules/flowbite/dist/flowbite.js
index 92e66fc..3659e36 100644
--- a/node_modules/flowbite/dist/flowbite.js
+++ b/node_modules/flowbite/dist/flowbite.js
@@ -32,7 +32,7 @@ const rotateAccordionIcon = accordionHeaderEl => {
}
};
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('turbo:load', () => {
document.querySelectorAll('[data-accordion]').forEach(function (accordionEl) {
const accordionId = accordionEl.getAttribute('id');
const collapseAccordion = accordionEl.getAttribute('data-accordion');
@@ -109,7 +109,7 @@ const toggleCollapse = (elementId, show = true) => {
}
};
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('turbo:load', () => {
// Toggle target elements using [data-collapse-toggle]
document.querySelectorAll('[data-collapse-toggle]').forEach(function (collapseToggleEl) {
var collapseId = collapseToggleEl.getAttribute('data-collapse-toggle');
@@ -150,7 +150,7 @@ const toggleModal = (modalId, show = true) => {
};
window.toggleModal = toggleModal;
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('turbo:load', () => {
document.querySelectorAll('[data-modal-toggle]').forEach(function (modalToggleEl) {
var modalId = modalToggleEl.getAttribute('data-modal-toggle');
var modalEl = document.getElementById(modalId);
@@ -172,7 +172,7 @@ document.addEventListener('DOMContentLoaded', () => {
/***/ 454:
/***/ (() => {
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('turbo:load', () => {
document.querySelectorAll('[data-tabs-toggle]').forEach(function (tabsToggleEl) {
const tabsToggleElementsId = tabsToggleEl.getAttribute('id');
const tabsToggleElements = document.querySelectorAll('#' + tabsToggleElementsId + ' [role="tab"]');
@@ -2208,7 +2208,7 @@ var popper_createPopper = /*#__PURE__*/popperGenerator({
;// CONCATENATED MODULE: ./src/components/dropdown.js
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('turbo:load', () => {
// Toggle dropdown elements using [data-dropdown-toggle]
document.querySelectorAll('[data-dropdown-toggle]').forEach(function (dropdownToggleEl) {
const dropdownMenuId = dropdownToggleEl.getAttribute('data-dropdown-toggle');
@@ -2256,7 +2256,7 @@ var tabs = __webpack_require__(454);
var modal = __webpack_require__(508);
;// CONCATENATED MODULE: ./src/components/tooltip.js
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('turbo:load', () => {
// Toggle dropdown elements using [data-dropdown-toggle]
document.querySelectorAll('[data-tooltip-target]').forEach(function (tooltipToggleEl) {
const tooltipEl = document.getElementById(tooltipToggleEl.getAttribute('data-tooltip-target'));
This issue body was partially generated by patch-package.
Thanks for posting this. I tested your solution on a local version of flowbite.js and it indeed fixed the problem I was having with the js not working. (also on Rails 7)
thanks :)
works like a charm 👍
Worked for me on rails7 project, thanks :)
Wow thanks for posting this. Was just hooking up flowbite with rails 7 and hit this issue. Glad I stumbled on this. Fixed my issues.
Also works with Turbolinks (5.2.0) with turbolinks:load
. Thanks!
For those using Webpack, a solution that works is to replace DOMContentLoaded by turbo:load in the ./node_modules/flowbite/dist/flowbite.js file
const replace = require('replace');
replace({
regex: "DOMContentLoaded",
replacement: "turbo:load",
paths: ['./node_modules/flowbite/dist/flowbite.js'],
recursive: true,
silent: true,
});
this is old, but it would be easier if inside your own js, you trigger the DOMContentLoaded event inside the turbo:load event
// import "flowbite";
window.document.addEventListener('turbo:load', (event) => {
// trigger flowbite events
window.document.dispatchEvent(new Event("DOMContentLoaded", {
bubbles: true,
cancelable: true
}));
});
Wouldn't it be even better if flowbite.js would have a .connect()
method, that we could use it like this:
// import "flowbite";
window.document.addEventListener('turbo:load', (event) => {
Flowbite.connect();
});
So no need to hack the flowbite.js and no need to trigger DOMContentLoaded
again, as that could lead to other side effets?!
UPDATE: Or as an alternative: Provide an event that reconnects flowbite when triggered. like "document flowbite:reconnect" or something like that.
this is old, but it would be easier if inside your own js, you trigger the DOMContentLoaded event inside the turbo:load event
// import "flowbite"; window.document.addEventListener('turbo:load', (event) => { // trigger flowbite events window.document.dispatchEvent(new Event("DOMContentLoaded", { bubbles: true, cancelable: true })); });
Yeah, that's a nice solution, I was considering something similar, but couldn't get it to not fire "DOMContentLoaded" twice on the initial load of the page (when "DOMContentLoaded" and "turbo:load" both fire). Do you know a way to solve this? Will appreciate it!
You can handle with Turbo Events with application.js as below. For example, You want to use Dismiss.
// this method copy from dismiss.js
function initDismiss() {
document.querySelectorAll('[data-dismiss-target]').forEach(triggerEl => {
const targetEl = document.querySelector(triggerEl.getAttribute('data-dismiss-target'))
new Dismiss(targetEl, {
triggerEl
})
})
}
document.addEventListener('turbo:load', initDismiss)
flowbite.js can export init method for example, initDismiss, initCollapse etc..? If so, We can handle Turbo Event at application.js as below.
document.addEventListener('turbo:load', initDismiss)
On 1.5.2 I've got the following:
window.document.addEventListener('turbo:load', (_event) => {
console.log("binding turbo:load to domcontentloaded")
window.document.dispatchEvent(new Event("DOMContentLoaded", {
bubbles: true,
cancelable: true
}));
});
window.document.addEventListener('DOMContentLoaded', (event) => {
console.log("domcontentloaded event");
})
I can see the domcontentloaded event
on every page change, so it's working as intended, but flowbite component JS is not working. Is there a different event I should be dispatching?
Update
I had to modify @TsubasaKawajiri's comment but got it working.
function initDismisses() {
document.querySelectorAll('[data-dismiss-toggle]').forEach(triggerEl => {
const targetEl = document.getElementById(triggerEl.getAttribute('data-dismiss-toggle'));
new Dismiss(targetEl, { triggerEl });
});
}
function initCollapses() {
document.querySelectorAll('[data-collapse-toggle]').forEach(triggerEl => {
const targetEl = document.getElementById(triggerEl.getAttribute('data-collapse-toggle'));
new Collapse(targetEl, { triggerEl });
})
}
window.document.addEventListener('turbo:load', (_event) => {
initDismisses();
initCollapses();
});
It works but it would be awfully nice to have a generic shim instead of copying init lines for every component.
Also getting this with Phoenix LiveView, I assume for the same reasons.
For LiveView the fix is similar:
window.addEventListener('phx:page-loading-stop', (event) => {
// trigger flowbite events
window.document.dispatchEvent(new Event("DOMContentLoaded", {
bubbles: true,
cancelable: true
}));
});
Please help me, how to use Flowbite and Turbo Rails 7 with import map? I use Flowbite v.1.5.3 as here https://flowbite.com/docs/getting-started/rails/ and tailwindcss
I change "DOMContentLoaded" on "turbo:load" in dist/flowbite.js and in vendor/javascript/flowbite.js, but it's not work for me, my scripts work only if reload page
Please help me, how to use Flowbite and Turbo Rails 7 with import map? I use Flowbite v.1.5.3 as here https://flowbite.com/docs/getting-started/rails/ and tailwindcss
I change "DOMContentLoaded" on "turbo:load" in dist/flowbite.js and in vendor/javascript/flowbite.js, but it's not work for me, my scripts work only if reload page
@VarProg I was having the same issue, and tried the solutions above with no success. Nevertheless, after checking a lot of similar problems I found something that might be helpful for us using Flowbite with Rails 7 as a temporary solution.
On your application.js add the following lines:
import "flowbite"
import { Turbo } from "@hotwired/turbo-rails"
Turbo.session.drive = false
According to the turbo.hotwired.dev handbook:
If you want Drive to be opt-in rather than opt-out, then you can set
Turbo.session.drive = false
; then,data-turbo="true"
is used to enable Drive on a per-element basis.
I also tried just setting the interactive element as data-turbo="false"
, without a positive outcome.
The solution mentioned works for me, but it is by no means a permanent solution, because the Rails application has bigger transition times during the navigation after I did this change. However, it can help more experienced developers to identify a better solution (I have been working with Rails for only one month).
I'll copy the link of that documentation for further reference. Turbo Handbook
I hope you find this solution useful, in the meantime.
Hey, IMHO this change introduced the bug https://github.com/themesberg/flowbite/commit/87a0c75d740c4e00739a91fe0167e39b35b1947b
I ended up using stimulus to set up Flowbite functions. The example below is for a Dropdown element's functionality.
app/views/shared/_nav.html.erb
<div data-controller="dropdown">
<button data-dropdown-target="trigger">
<div data-dropdown-target="menu">
<!--- all the popover html -->
</div>
</div>
app/javascripts/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["trigger", "menu"]
connect() {
this.dropdown = new Dropdown(this.menuTarget, this.triggerTarget)
}
}
app/javascripts/application.js
import "@hotwired/turbo-rails"
import "flowbite"
import "controllers"
This is a really great idea! Thank you @vladiim !
@indigodavid Working, not sure thats the correct solution tho. Thanks :D
Ruby on Rails 7 support for turbo load is now available starting from the v1.5.5
of the library: https://github.com/themesberg/flowbite/releases/tag/v1.5.5a
Please update via NPM to the latest version and instead of importing flowbite
try to import import "flowbite/src/flowbite.turbo"
where we use @filser89's suggestion of adding turbo:load
.
Additionally, you can also use the CDN directly starting from v1.5.5:
<script src="https://unpkg.com/[email protected]/dist/flowbite.turbo.js"></script>
Please re-open if facing difficulties with the new version.
I came here after following the Flowbite Rails installation instructions as I'm getting the following error:
Uncaught TypeError: Failed to resolve module specifier "flowbite/src/flowbite.turbo.js". Relative references must start with either "/", "./", or "../".
What I can confirm so far
- Tailwind is working
- Flowbite 1.5.5 is installed via the latest
npm
(v.9.2.0) -
tailwind.config.js
has been modified as directed -
import "flowbite"
added toapp/javascript/application.js
Things are working up to this point.
Things go wrong
-
Added
import "flowbite/src/flowbite.turbo.js"
toapplication.js
(emphasis explained in next section) - Ran
./bin/importmap pin flowbite
- Restarted the server and re-ran
./bin/dev
At this point I'm getting the error which is causing other scripts to not load properly
Potential conflicts
The site's instructions conflict with the previous post. @zoltanszogyenyi says to import "flowbite/src/flowbite.turbo"
instead of flowbite
. Note that the .js
is left off too.
I've tried all combinations of removing/including import "flowbite"
and importing flowbite.turbo
and flowbite.turbo.js
but none have succeeded.
I'll keep fiddling around and report back with clearer instructions should I figure out what's going on.
Hey @agrberg,
Thanks for the report - I'll check this tomorrow. The new flowbite.turbo.js should have the turbo:load event listeners hence why it now should work.
Try importing flowbite/dist/flowbite.turbo.js instead.
Thanks, I'll check back then as well. FWIW the dist
version w/ .js
doesn't work either:
Uncaught TypeError: Failed to resolve module specifier "flowbite/dist/flowbite.turbo.js". Relative references must start with either "/", "./", or "../".
Edit I tried re-doing it and am now getting a few new error 😅
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/css". Strict MIME type checking is enforced for module scripts per HTML spec.
It's coming from flowbite.css:1
which contains (removed the commented quote for space)
@tailwind base;
@tailwind components;
@tailwind utilities;
I'm not sure why this is being brought in considering this is the same as app/assets/stylesheets/application.tailwind.css
.
Hey @agrberg,
It's a Friday night, but I couldn't sleep on this one :)
I went ahead and tested it on my local Ruby on Rails configuration - and you're right, the relative paths are incorrect because it automatically imports what we have declared as "main" in our package.json
file.
Here's a solution:
In your importmap.rb
file add the following line:
pin "flowbite", to: "https://unpkg.com/[email protected]/dist/flowbite.turbo.js"
This will map the turbo ready file with turbo:load
event listeners instead of the default one.
Then import Flowbite in your application.js
file like this:
import "flowbite"
This should work, unless I have something not configured properly. This is tested with turbo load as well, meaning the event listeners work throughout navigating between the pages.
Please try this out and if it works I'll update the documentation accordingly. Thanks!
🙇 Thank you @zoltanszogyenyi! That did it 🚀
For reference ./bin/importmaps pin flowbite
adds the following to importmap.rb
pin "flowbite", to: "https://ga.jspm.io/npm:[email protected]/src/flowbite.js"
pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/[email protected]/lib/index.js"
I've changed the first line to what you've suggested and it is working. I haven't touch the popperjs line and I need to learn the differences between jspm.io and unpkg.com as importmaps are still generally new to me.
I've now changed the docs to recommend turbo support first because the standard flowbite.js
file hooks to the window load event instead of turbo:load
. I hope this will make it clear for those who want to use Ruby on Rails 7 in the future.
The problem with jspm.io here is that I can't tell it via package.json
to include a separate file, meaning flowbite.turbo.js
instead of flowbite.js
in order to set up the specific event listeners (ie. turbo-load
).
I'll declare this issue closed now.
Is there a resolution if you are not using importmaps? I still have to add <meta name="turbo-visit-control" content="reload">
in order for flowbite to work on subsequent pages.
I have "flowbite": "1.5.5"
installed and am using import "flowbite"
in application.js.
When should flowbite
do init work
In the index.ts
, I can see
const events = new Events('load', [
initAccordions,
initCarousels,
initCollapses,
initDismisses,
initDropdowns,
initModals,
initDrawers,
initTabs,
initTooltips,
initPopovers,
initDials,
]);
events.init();
What if some people still want to do init on DOMContentLoaded
event?
Current Solution is Bad
I do not understand why we have files like this
index.ts
index.turbo.ts
And this
datepicker.js
datepicker.turbo.js
Does that mean we will add extra files if we need to support another framework in the future?
The REAL problem
flowbite
rely on hard coded event name
to do init work and do not support a way to let developers to change it.
What about this solution:
-
flowbite
read value fromwindow.FLOWBITE_INIT_EVENT_NAME
, the fallback value isload
- Then the code use
window.FLOWBITE_INIT_EVENT_NAME
to do init work.
This solution can solve all the above problems.
Hey @michael-yin,
Thanks for your input on this, it's much appreciated!
I think that one reason why the separate files solution is good is because it's easy to get started with it without requiring to do any other configuration and we can also test it before making a release with different frameworks.
On the other hand, as you've stated, it's less flexible when it comes to adding your own object and event load listeners.
I'm currently focusing on some other issues for a v1.6.1
release but you're more than welcome to open up a PR with an alternative solution!
Either way this will definitely be looked into.
Cheers, Zoltan