TW-Elements icon indicating copy to clipboard operation
TW-Elements copied to clipboard

Any JS based component not working with Hotwired Turbo

Open sahilas opened this issue 1 year ago • 18 comments

When it installed on top of Hotwire (Turbo) the JS getting broken due to import of tailwindcss-element into tailwindcss.config is by node module path specific not global initializer

eg component:

import { Datepicker, Input, initTE, } from "tw-elements";

initTE({ Datepicker, Input });

sahilas avatar Jun 13 '23 17:06 sahilas

Hi! Did you solve the problem?

aka-nez avatar Aug 10 '23 18:08 aka-nez

I've ended up with a custom event listener

import {Dropdown, initTE} from "tw-elements";

addEventListener("turbo:load", event => {
    initTE({ Dropdown })
})

aka-nez avatar Aug 10 '23 19:08 aka-nez

So I found the issue.

As Turbo does not reload the full page but replaces the body from the response in the XHR.

initiatedComponents which was a global holds the states of each components. But this PR removed it.

I figured out also that I needed to add initiatedComponents.splice(0, initiatedComponents.length)

import {Collapse, Dropdown, initTE, Input, Ripple, Toast, Sidenav,} from "tw-elements";

const initTailwindElement = () => {
  // Reset the initiatedComponents array on turbo:load
  initiatedComponents.splice(0, initiatedComponents.length)
  initTE({Input, Ripple, Dropdown, Collapse, Toast, Sidenav,});
}

document.addEventListener("turbo:load", initTailwindElement)

Now with the rewrite with the register it is not possible to do so. One possibility is to make the register global, so we can reset it (looks a bit fishy to me anyway)

Wdyt @smolenski-mikolaj ?

makikata avatar Sep 08 '23 06:09 makikata

So I found that we can use the v1.0.0 that has this nice fix: https://github.com/mdbootstrap/Tailwind-Elements/pull/1919/files

Thank you!

makikata avatar Sep 08 '23 06:09 makikata

Hi @makikata , the v1.0.0 is going to be released on monday, 11.09.2023

juujisai avatar Sep 08 '23 06:09 juujisai

@juujisai is this bug fixed? am using turbo + te The page works fine in the first loaded. If go to previous/next page, js seems not init. I've add the log in addEventListener, seems the initTE({ Select }) has been inited, but the DOM still broke.

https://github.com/mdbootstrap/Tailwind-Elements/assets/10682522/03bbf545-dd4e-43ca-9379-6fa71c95611e

code

import {Select, initTE} from "tw-elements";

addEventListener("turbo:load", () => {
    console.log("!!!!!!!!!!!!!!!!!binding turbo:load to domcontentloaded!!!!!!!!!!");
    console.log(document.readyState);
    initTE({ Select })
})

addEventListener('DOMContentLoaded', (event) => {
    console.log("DOMContentLoaded event");
    //initTE({ Select });
})

import '@hotwired/turbo-rails';

Any suggestion?

ponponwu avatar Sep 20 '23 08:09 ponponwu

@ponponwu Try adding options to initTE

initTE({ Select }, { allowReinits: true });

juujisai avatar Sep 20 '23 10:09 juujisai

Hi @juujisai , thanks for your response I tried adding initTE({ Select }, { allowReinits: true });

but if I go to previous page then next page, this situation may happens

image

ponponwu avatar Sep 20 '23 11:09 ponponwu

Hi @ponponwu. Without the allowReinits option, after inspecting the select element, was there an wrapper element with data-te-select-wrapper-ref tag or just the native select?

Maybe wrapping the initTE method inside a if statement could help here? Something like:

const selectEl = document.querySelector([data-te-select-wrapper-ref])
if (!selectEl ) {
initTE({ Select }, { allowReinits: true });
}

Other think I can think of is to dispose the instance before calling then initialize the component via JS inside turbo:load listener. Something like this (need to be tested):

const selectEl = document.querySelector("#mySelect");
const selectInstance = Select.getInstance(selectEl);

if (selectInstance) {
  selectInstance.dispose();
}
new Select(selectEl);

juujisai avatar Sep 21 '23 06:09 juujisai

@juujisai thanks again for your time! both ways not working. The main problem is that I can't get element with this line const selectEl = document.querySelector([data-te-select-wrapper-ref]) I may change another UX instead, thanks a lot!

ponponwu avatar Sep 22 '23 10:09 ponponwu

@ponponwu I listen to turbo:frame-render if it is inside a turbo-frame

document.addEventListener('turbo:frame-render', initTailwindElement)

makikata avatar Sep 30 '23 06:09 makikata

This worked well for me.

import { Select, initTE } from "tw-elements";

addEventListener("turbo:load", event => {
  initTE({ Select })
})

addEventListener("turbo:frame-render", event => {
  initTE({ Select }, { allowReinits: true })
})

import '@hotwired/turbo-rails'

Sohair63 avatar Oct 10 '23 18:10 Sohair63

I ended up using stimulus to solve this according to @juujisai 's suggestion

e.g. 
static targets = ["selector"];

connect() {
  const selectEl = this.selectorTarget;
  const selectInstance = Select.getInstance(selectEl);

  if (selectInstance) {
    selectInstance.dispose();
  }
  const select = new Select(selectEl);
}

thanks everyone!

ponponwu avatar Oct 18 '23 06:10 ponponwu

I tried all above solutions and for some reason the selects and inputs were still breaking for me, but I could fix them with the following code:

import { Dropdown, Collapse, Select, Input, Ripple, initTE } from "tw-elements";

initTE({ Dropdown, Collapse, Ripple })

addEventListener("turbo:load", event => {
  document.querySelectorAll("[data-te-select-init]").forEach((el) => {
    new Select(el);
  })
  document.querySelectorAll("[data-te-input-wrapper-init]").forEach((el) => {
    new Input(el);
  })
});

websebdev avatar Dec 17 '23 02:12 websebdev

Hi, i'm using unpoly with tw-elements and faced similar issue. I want to add my workaround just in case someone else using unpoly will come here.

    <script type="module">
      const elements = await import('https://cdn.jsdelivr.net/npm/tw-elements/dist/js/tw-elements.es.min.js');
      const elementsConfig = {
        Alert: "[data-te-alert-init]",
        Animate: "[data-te-animation-init]",
        Carousel: "[data-te-carousel-init]",
        ChipsInput: "[data-te-chips-input-init]",
        Chip: "[data-te-chip-init]",
        Datepicker: "[data-te-datepicker-init]",
        Datetimepicker: "[data-te-date-timepicker-init]",
        Input: "[data-te-input-wrapper-init]",
        PerfectScrollbar: "[data-te-perfect-scrollbar-init]",
        Rating: "[data-te-rating-init]",
        ScrollSpy: "[data-te-spy='scroll']",
        Select: "[data-te-select-init]",
        Sidenav: "[data-te-sidenav-init]",
        Stepper: "[data-te-stepper-init]",
        Timepicker: "[data-te-timepicker-init]",
        Toast: "[data-te-toast-init]",
        Datatable: "[data-te-datatable-init]",
        Popconfirm: "[data-te-toggle='popconfirm']",
        Validation: "[data-te-validation-init]",
        SmoothScroll: "a[data-te-smooth-scroll-init]",
        LazyLoad: "[data-te-lazy-load-init]",
        Clipboard: "[data-te-clipboard-init]",
        InfiniteScroll: "[data-te-infinite-scroll-init]",
        LoadingManagement: "[data-te-loading-management-init]",
        Sticky: "[data-te-sticky-init]",
        MultiRangeSlider: "[data-te-multi-range-slider-init]",
        Chart: "[data-te-chart]",
        Button: "[data-te-toggle='button']",
        Collapse: "[data-te-collapse-init]",
        Dropdown: "[data-te-dropdown-toggle-ref]",
        Modal: "[data-te-toggle='modal']",
        Ripple: "[data-te-ripple-init]",
        Offcanvas: "[data-te-offcanvas-toggle]",
        Tab: "[data-te-toggle='tab'], [data-te-toggle='pill'], [data-te-toggle='list']",
        Tooltip: "[data-te-toggle='tooltip']",
        Popover: "[data-te-toggle='popover']",
        Lightbox: "[data-te-lightbox-init]",
        Touch: "[data-te-touch-init]",
      };

      Object.keys(elementsConfig).forEach(key => {
        up.compiler(elementsConfig[key], function(element){
          new elements[key](element);
        });
      });
    </script>

vb8448 avatar Jan 02 '24 19:01 vb8448

HI @vb8448, keep in mind some components have their callbacks which are required for proper initialisation. We recommend using the initTE method everywhere possible.

iprzybysz avatar Jan 03 '24 12:01 iprzybysz

@iprzybysz I wasn't able to make initTE work, I'll give another chances later. The problem I see with initTE is that it search for elements to init in the entire DOM, and it sounds wrong to rerun it every time on every DOM change.

I guess I can update a little bit the initTE function like this, allowing to search for new elements to init only in a subsection of the DOM, what do you think?

const defaultOptions = {
  allowReinits: false,
  checkOtherImports: false,
  document: document,                           <= NEW
};

const initTE = (components, options = {}) => {
  options = { ...defaultOptions, ...options };

  const componentList = Object.keys(defaultInitSelectors).map((element) => {
    const requireAutoinit = Boolean(
      options.document.querySelector(defaultInitSelectors[element].selector)                            <= NEW
    );
   ......

vb8448 avatar Jan 03 '24 13:01 vb8448

We also think about something similar to what you suggest. We may add it in the future.

iprzybysz avatar Jan 04 '24 07:01 iprzybysz