flowbite icon indicating copy to clipboard operation
flowbite copied to clipboard

Flowbite with Turbo on rails7

Open filser89 opened this issue 3 years ago • 14 comments

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.

filser89 avatar Feb 09 '22 14:02 filser89

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)

seanbjornsson avatar Feb 23 '22 22:02 seanbjornsson

thanks :)

codenashwan avatar Mar 23 '22 16:03 codenashwan

works like a charm 👍

kkurcz avatar Mar 30 '22 09:03 kkurcz

Worked for me on rails7 project, thanks :)

skipmaple avatar Apr 08 '22 02:04 skipmaple

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.

spacerobotTR avatar May 28 '22 12:05 spacerobotTR

Also works with Turbolinks (5.2.0) with turbolinks:load. Thanks!

puglet5 avatar Jun 28 '22 00:06 puglet5

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,
});

blump avatar Jul 05 '22 22:07 blump

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
    }));
});

jsbaltodano avatar Jul 20 '22 00:07 jsbaltodano

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.

mikelieser avatar Jul 20 '22 06:07 mikelieser

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!

filser89 avatar Aug 03 '22 10:08 filser89

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)

TsubasaKawajiri avatar Aug 08 '22 06:08 TsubasaKawajiri

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.

archonic avatar Sep 01 '22 06:09 archonic

Also getting this with Phoenix LiveView, I assume for the same reasons.

Rodeoclash avatar Sep 15 '22 06:09 Rodeoclash

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
  }));
});

egze avatar Sep 15 '22 15:09 egze

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

IndieVar avatar Oct 05 '22 18:10 IndieVar

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.

indigodavid avatar Oct 13 '22 04:10 indigodavid

Hey, IMHO this change introduced the bug https://github.com/themesberg/flowbite/commit/87a0c75d740c4e00739a91fe0167e39b35b1947b

sevos avatar Nov 06 '22 01:11 sevos

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"

vladiim avatar Nov 07 '22 02:11 vladiim

This is a really great idea! Thank you @vladiim !

sevos avatar Nov 07 '22 13:11 sevos

@indigodavid Working, not sure thats the correct solution tho. Thanks :D

fernandokbs avatar Dec 03 '22 22:12 fernandokbs

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.

zoltanszogyenyi avatar Dec 08 '22 15:12 zoltanszogyenyi

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 to app/javascript/application.js

Things are working up to this point.

Things go wrong

  • Addedimport "flowbite/src/flowbite.turbo.js" to application.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.

agrberg avatar Dec 16 '22 19:12 agrberg

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.

zoltanszogyenyi avatar Dec 16 '22 19:12 zoltanszogyenyi

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.

agrberg avatar Dec 16 '22 19:12 agrberg

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!

zoltanszogyenyi avatar Dec 16 '22 21:12 zoltanszogyenyi

🙇 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.

agrberg avatar Dec 16 '22 21:12 agrberg

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.

zoltanszogyenyi avatar Dec 16 '22 21:12 zoltanszogyenyi

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.

cmalpeli avatar Dec 17 '22 15:12 cmalpeli

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:

  1. flowbite read value from window.FLOWBITE_INIT_EVENT_NAME, the fallback value is load
  2. Then the code use window.FLOWBITE_INIT_EVENT_NAME to do init work.

This solution can solve all the above problems.

michael-yin avatar Jan 10 '23 09:01 michael-yin

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

zoltanszogyenyi avatar Jan 10 '23 10:01 zoltanszogyenyi