themesberg / flowbite

Open-source UI component library and front-end development framework based on Tailwind CSS
https://flowbite.com
MIT License
7.29k stars 708 forks source link

Phoenix Liveview: Flowbite elements that require js do not work in content pushed to the client after initial page load #869

Open DennisNissen opened 2 months ago

DennisNissen commented 2 months ago

Describe the bug Flowbite elements that require javascript do not work or stop working when added to the dom with a liveview update. This is especially uncomfortable when using async data fetching and using flowbite components (f.e. Accordions) or when you update dom elements with server pushes.

Expected behavior Flowbite elements do always work, even when added to the dom with a liveview update.

Desktop (please complete the following information):

Additional context

Example:

<.async_result :let={time_entries} assign={@time_entries}>
  <:loading>
    <div class="flex items-center mt-5">
      <span class="loading loading-spinner loading-lg"></span>&nbsp; Loading time entries and combining them with gitlab issues
    </div>
  </:loading>
  <:failed :let={_failure}>Error loading time entries or gitlab issues</:failed>

 <div data-accordion="collapse" id="example-accordion">
   <button data-accordion-target={"#detail-#{issue}"}>Click me</button>
   <div class="hidden" id={"detail-#{issue}"}>
     important informations
   </div>
 </div>
</.async_result> 

The example-accordion's content is pushed to the client after the async data loading has finished and flowbite elements in the result will not work.

Liveview's dom hook has the ability to apply javascript to the dom elements that are pushed to the client with

let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  dom: {
    onBeforeElUpdated(from, to) {
     //copy attributes from "from" to "to" or apply function on "to"  
    }
  }
})

But the init.... (f.e. initAccordions) functions are using document.querySelectorAll() and thus are unable to be scoped to the incoming DOM nodes.

A workaround for now is to apply a custom hook to the parent node

const Hooks = {}
Hooks.Flowbite = {
  mounted(){
    initFlowbite();
  }
  updated(){
    this.mounted()
  }
}

let liveSocket = new LiveSocket("/live", Socket, {
    params: {_csrf_token: csrfToken}, hooks: Hooks
})

and in the template file

...
<div phx-hook="Flowbite">
 <div data-accordion="collapse" id="example-accordion">
   <button data-accordion-target={"#detail-#{issue}"}>Click me</button>
   <div class="hidden" id={"detail-#{issue}"}>
     important informations
   </div>
 </div>
</div>
...

This works, but triggers a lot of init functions for each update. It would be great to be able to apply the init functions to the dom elements that are updated.

tduffield commented 2 weeks ago

So there is a slightly more fine-grained workaround. Rather than running initFlowbite, you can call the specific init function for the component you need. For example, for me it was tabs.

const Hooks = {}
Hooks.FlowbiteTabs = {
  mounted(){
    window.initTabs();
  }
  updated(){
    this.mounted()
  }
}