This guide adds a data grid to a Phoenix 1.8 app with LiveView 1.2 (a Phoenix 1.7 app with LiveView 1.1 works the same way). At the end, a LiveView shows its users in a grid that sorts, searches and scrolls, and then the orders of a table, read from the database a block at a time.

Your app does not run Node.js for Masu. The Hex package has the built bundle, and your esbuild profile imports it from deps/.

1. Add the dependency

In mix.exs:

def deps do
  [
    {:masu, "~> 0.1"}
  ]
end

Then run mix deps.get.

2. Run the installer

mix masu.install --dry-run   # shows the changes as a diff
mix masu.install

The installer changes two files, and prints what it changed:

  • assets/js/app.js: it adds import { Masu } from "masu" and puts Masu in the hooks of the LiveSocket.
  • assets/css/app.css: it adds the Masu style sheet, and the Sumi theme import as a comment.

It does not add what a file already has, so you can run it again. When it cannot find the new LiveSocket(...) call, or its hooks are not an object or a variable, it changes nothing in app.js and prints the lines to add.

3. Import the components

In the html_helpers of your web module (lib/my_app_web.ex):

import Masu.Components

4. A grid of a list

defmodule MyAppWeb.UserLive.Index do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, users: Accounts.list_users())}
  end

  def render(assigns) do
    ~H"""
    <.data_grid id="users" rows={@users} label="Users" searchable>
      <:col field={:name} />
      <:col field={:email} />
      <:col field={:inserted_at} type={:datetime} label="Joined" />
    </.data_grid>
    """
  end
end

The grid shows the first rows at once, rendered on the server. A click on a header sorts by it, the search box finds rows by their text, and the arrow keys move between the cells.

5. A grid of a table

For a table of many rows, give the grid an Ecto query and the repo instead of the rows:

<.data_grid
  id="orders"
  source={from(o in Order, where: o.shop_id == ^@shop.id)}
  repo={MyApp.Repo}
  label="Orders"
  sort={[{:placed_at, :desc}]}
>
  <:col field={:number} width={120} />
  <:col field={:customer} />
  <:col :let={order} field={:status} type={:enum}>
    <.badge status={order.status} />
  </:col>
  <:col field={:total} type={:decimal} />
  <:col field={:placed_at} type={:datetime} label="Placed" />
</.data_grid>

Now the database sorts, searches and counts, and the LiveView reads only the rows the person scrolls to: a hundred at a time, with LIMIT and OFFSET. See Sources for what the query may have, and the indexes that make it fast.

6. Test it

import Masu.Test

test "lists the newest orders first", %{conn: conn} do
  {:ok, view, _html} = live(conn, ~p"/orders")

  assert [%{"number" => "A-1042"} | _] = rows(view, "orders")
  assert total(view |> search("orders", "acme"), "orders") == 3
end

See Testing.