Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Any JS based component not working with Hotwired Turbo #1703

Open
sahilas opened this issue Jun 13, 2023 · 18 comments
Open

Any JS based component not working with Hotwired Turbo #1703

sahilas opened this issue Jun 13, 2023 · 18 comments

Comments

@sahilas
Copy link

sahilas commented Jun 13, 2023

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

@aka-nez
Copy link

aka-nez commented Aug 10, 2023

Hi! Did you solve the problem?

@aka-nez
Copy link

aka-nez commented Aug 10, 2023

I've ended up with a custom event listener

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

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

@makikata
Copy link

makikata commented Sep 8, 2023

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
Copy link

makikata commented Sep 8, 2023

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!

@juujisai
Copy link
Contributor

juujisai commented Sep 8, 2023

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

@ponponwu
Copy link

ponponwu commented Sep 20, 2023

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

2023-09-20.4.47.25.mov

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?

@juujisai
Copy link
Contributor

@ponponwu Try adding options to initTE

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

@ponponwu
Copy link

ponponwu commented Sep 20, 2023

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

@juujisai
Copy link
Contributor

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

@ponponwu
Copy link

@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!

@makikata
Copy link

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

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

@Sohair63
Copy link

Sohair63 commented Oct 10, 2023

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'

@sahilas sahilas changed the title Any JS based component not working with Rails + Turbo Any JS based component not working with Hotwire Turbo Oct 16, 2023
@sahilas sahilas changed the title Any JS based component not working with Hotwire Turbo Any JS based component not working with Hotwired Turbo Oct 16, 2023
@ponponwu
Copy link

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!

@websebdev
Copy link

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

@vb8448
Copy link

vb8448 commented Jan 2, 2024

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>

@iprzybysz
Copy link
Contributor

iprzybysz commented Jan 3, 2024

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

@vb8448
Copy link

vb8448 commented Jan 3, 2024

@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
    );
   ......

@iprzybysz
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants