TW-Elements
TW-Elements copied to clipboard
Any JS based component not working with Hotwired Turbo
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 });
Hi! Did you solve the problem?
I've ended up with a custom event listener
import {Dropdown, initTE} from "tw-elements";
addEventListener("turbo:load", event => {
initTE({ Dropdown })
})
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 ?
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!
Hi @makikata , the v1.0.0 is going to be released on monday, 11.09.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.
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 Try adding options to initTE
initTE({ Select }, { allowReinits: true });
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
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 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 I listen to turbo:frame-render
if it is inside a turbo-frame
document.addEventListener('turbo:frame-render', initTailwindElement)
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'
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!
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);
})
});
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>
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 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
);
......
We also think about something similar to what you suggest. We may add it in the future.