How to Integrate Phoenix LiveView with DataTables.js Without DOM Conflicts?
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:
Items
ItemLive
(rendered in .eex
via live_render
)DataTableComponent
(rendered in ItemLive
via live_component
)handle_event("filter", params, socket)
updates filtered_rows
in the socket assignsI want LiveView to handle dynamic updates (e.g., filtering) while keeping DataTables.js functional for its features (sorting, pagination, etc.), without DOM conflicts.
Using phx-update="ignore"
on the DataTable
push_event(socket, "render-datatable-payload", %{})
.Destroying/Reinitializing DataTables
push_event
to notify the JS hook to destroy the DataTable before LiveView updates the DOM, then reinitialize it.updated
hook runs after the DOM update, and the event arrives too late, causing timing issues. Performance also concerns me.Ditching DataTables for a Native Solution
Hybrid Approach
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 }
});
Any advice or examples would be greatly appreciated!
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.
When LiveView updates the DOM, Datatable still needs to do some work to reflect the DOM changes. Datatable is managing its own DOM, so LiveView should not interfere with it. When you say phx-update="ignore"
you are telling LiveView not to update the DOM because you are going to do it yourself in your hook, on the client side.
The only flow that I can think of that would work is to push the new data using push_event
and handle it on the client side.
// datatable_hook.js
const DataTableInit = {
mounted() {
this.initDataTable();
this.handleEvent("datatable-update", ({ rows }) => {
this.dataTable.clear();
this.dataTable.rows.add(rows);
this.dataTable.draw(false);
});
},
initDataTable() {
const tableId = `#${this.el.id}`;
const options = JSON.parse(this.el.dataset.options || '{}');
this.dataTable = $(tableId).DataTable({ data: [], ...options });
}
};
//my_live_view.ex
def handle_event("filter-data", _params, socket) do
{:noreply, push_event(socket, "datatable-update", %{rows: get_table_rows(/* whatever_filter_you_like */)})}
end