Title
How to Integrate Phoenix LiveView with DataTables.js Without DOM Conflicts?
Versions
- Phoenix: 1.5.7
- LiveView: 0.15.7
- DataTables.js: 1.11.4
Problem
I’m using Phoenix LiveView to render a DataTable (via live_component
) within a static route. The LiveView module is rendered with live_render
(:not_mounted_at_router
) inside a static .eex
template. On initial mount, the DataTable works as expected. However, when handling an event (e.g., filtering rows), LiveView updates the DOM, overwriting the div.datatables_wrapper
, which causes DataTables.js to lose track of its state.
The relevant project structure is:
- Static Route:
Items
- LiveView:
ItemLive
(rendered in.eex
vialive_render
) - Component:
DataTableComponent
(rendered inItemLive
vialive_component
) - Event:
handle_event("filter", params, socket)
updatesfiltered_rows
in the socket assigns
Goal
I want LiveView to handle dynamic updates (e.g., filtering) while keeping DataTables.js functional for its features (sorting, pagination, etc.), without DOM conflicts.
What I’ve Tried
Using
phx-update="ignore"
on the DataTable- Idea: Let DataTables manage its DOM and push data via
push_event(socket, "render-datatable-payload", %{})
. - Issue: Offloads rendering to the client, reducing LiveView’s benefits. I’d prefer a server-driven solution.
- Idea: Let DataTables manage its DOM and push data via
Destroying/Reinitializing DataTables
- Idea: Use
push_event
to notify the JS hook to destroy the DataTable before LiveView updates the DOM, then reinitialize it. - Issue: The
updated
hook runs after the DOM update, and the event arrives too late, causing timing issues. Performance also concerns me.
- Idea: Use
Ditching DataTables for a Native Solution
- Idea: Use a Phoenix-native table library or custom implementation.
- Issue: DataTables.js is heavily used in this project, and replacing it risks breaking user-expected functionality. Not a preferred option unless necessary.
Hybrid Approach
- Idea: Let LiveView handle data updates and minimize client-side DOM manipulation.
- Issue: I’m struggling to make this work smoothly and need guidance.
Questions
- Has anyone successfully integrated LiveView with DataTables.js in a similar setup?
- How can I prevent LiveView from overwriting the DataTable DOM while still leveraging both tools?
- Are there known patterns or hooks to synchronize LiveView updates with DataTables reinitialization?
- Would upgrading to a newer LiveView version (e.g., 1.x) resolve this? (Upgrades are possible with justification.)
Relevant Code
item_live.ex
defmodule FulfillmentCartWeb.ItemLive do use FulfillmentCartWeb, :live_view def mount(:not_mounted_at_router, _session, socket) do {:ok, assign(socket, rows: Items.list_items(), options: options())} end def render(assigns) do ~L""" <%= live_component @socket, MyAppWeb.Components.DataTable, id: "item-table", rows: @rows, options: @options %> """ end defp options do %{ "stateSave" => true, "info" => false, "pageLength" => 15, "dom" => "Bfrtip", "buttons" => [%{"extend" => "csv", "text" => "Download CSV"}] } end end
data_table.js
(LiveView Hook)
const $ = window.jQuery; const DataTableInit = { mounted() { this.initDataTable(); }, updated() { // Issue: Runs after DOM update, too late to destroy/reinit DataTable }, initDataTable() { const tableId = `#${this.el.id}`; const options = JSON.parse(this.el.dataset.options || '{}'); $(tableId).DataTable({ ...options }); } }; export default DataTableInit;
app.js
let liveSocket = new LiveSocket("/live", Socket, { params: {_csrf_token: csrfToken}, hooks: { DataTableInit } });
Notes
- The issue only occurs on updates, not initial mount.
Any advice or examples would be greatly appreciated!