Categories
Elixir Software

Integrating Chartkick with Phoenix LiveView

I was converting some “old-fashioned” Phoenix code to a LiveView today, and got stuck for a while trying to get a Chartkick graph to render properly. I found various hints online about how to do it, and I assumed it was a case of phx-update="ignore" and some kind of Javascript hooks, but it took a bit of time to figure out the details. I thought I’d be helpful and write up an example, as it turns out it’s not that hard.

First let’s create the application. I skipped Ecto because we don’t need a database for this example.

mix archive.install hex phx_new
mix phx.new my_app --no-ecto

Run up the server and visit http://localhost:4000 to make sure it works:

The default Phoenix front page

First we’re going to get Chartkick running on a ‘static’ (ie not LiveView) page. Add {:chartkick, "~> 0.4"} to deps in mix.exs, then run mix deps.get.

Remove the boilerplate header from lib/my_app_web/templates/layout/root.html.heex, and add these lines to the head:

<script src="//www.google.com/jsapi"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartkick/1.3.0/chartkick.min.js"></script>

Replace the contents of lib/my_app_web/templates/page/index.html.heex with the following:

<%= "{foo: 1, bar: 4, baz: 2, qux: 3}"
|> Chartkick.bar_chart()
|> Phoenix.HTML.raw() %>

After restarting the server we have a chart!

A basic chart

So far, so good. Time to try it with a live view! Replace the root route in lib/my_app_web/router.ex with live "/", PageLive, and create the module in lib/my_app_web/live/page_live.ex, rendering exactly the same html as we had before:

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <%= "{foo: 1, bar: 4, baz: 2, qux: 3}"
    |> Chartkick.bar_chart()
    |> Phoenix.HTML.raw() %>
    """
  end
end

When this renders, we see the chart flash up briefly, then … hmm.

Perpetually loading

The reason we see the chart appear then disappear is that LiveView renders the page twice when it’s loaded – once statically, for search engines etc, then a second time, when it swaps in its dynamic Dom. Here are the elements that Chartkick initially creates (a placeholder div and a bit of javascript that will call the library and swap in the generated graph):

<div id="b5b7b558-1220-4546-a3e8-15e2e607b312" style="...">
  Loading...
</div>
<script type="text/javascript">
  new Chartkick.BarChart(
    'b5b7b558-1220-4546-a3e8-15e2e607b312',
    {foo: 1, bar: 4, baz: 2, qux: 3}, {});
</script>

The problem is that a script tag inserted dynamically into the Dom, unlike one in the original page source, doesn’t get executed. There’s also a potential issue with LiveView and Chartkick both trying to manipulate the same elements, leading to unpredictable behaviour when the page data is updated. We can address the latter issue by telling LiveView to ignore the chart when updating the page:

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <div id="chart" phx-update="ignore">
      <%= "{foo: 1, bar: 4, baz: 2, qux: 3}"
      |> Chartkick.bar_chart()
      |> Phoenix.HTML.raw() %>
    </div>
    """
  end
end

This actually works in our simple case, but what if the chart wasn’t always shown, or was part of a component that was live patched in? As the simplest possible illustration of this, let’s add some show/hide buttons and only render the element when we toggle @show to true:

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, show: false)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <input type="button" phx-click="hide" value="Hide"
      disabled={not @show} />
    <input type="button" phx-click="show" value="Show"
      disabled={@show} />
    <%= if @show do %>
      <div id="chart" phx-update="ignore">
        <%= "{foo: 1, bar: 4, baz: 2, qux: 3}"
        |> Chartkick.bar_chart()
        |> Phoenix.HTML.raw() %>
      </div>
    <% end %>
    """
  end

  @impl true
  def handle_event("hide", _params, socket) do
    {:noreply, assign(socket, show: false)}
  end

  def handle_event("show", _params, socket) do
    {:noreply, assign(socket, show: true)}
  end
end

Initially the chart isn’t there, as expected:

Hidden …

But when we click “show”, we’re back to our perpetual loading indicator:

… and still kind of hidden

We need a way of triggering the Chartkick script when the view’s updated, and that’s where hooks come in. First define a hook in assets/js/app.js (I’m not entirely sure this is the best way of running the javascript, but it works!):

let Hooks = {
  RenderChart: {
    mounted() { 
      console.log("RenderChart")
      console.log(this.el)
      eval(this.el.getElementsByTagName("script")[0].innerHTML)
    }
  }
}

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

Now bind that hook to the element:

<div id="chart" phx-update="ignore" phx-hook="RenderChart">
  <%= "{foo: 1, bar: 4, baz: 2, qux: 3}"
  |> Chartkick.bar_chart()
  |> Phoenix.HTML.raw() %>
</div>

Tada!

All working!

There are other hooks as well as mounted – If we were dynamically updating the chart element itself we’d probably want to also execute the script on updated, for example.

And that’s it! The code’s here, and I hope it helps someone avoid a bit of head scratching.

One reply on “Integrating Chartkick with Phoenix LiveView”

[…] I’m not really sure much has happened since last time. I spent a couple of hours working out how to make Chartkick.js play nicely with Phoenix LiveView, and because I was feeling public-spirited and there wasn’t much detail around of how to do it (I think it’s one of those things that’s just simple enough that if you look on forums you see people asking, being given a hint then coming back saying “thanks, that made sense” without explaining exactly what they did) I decided to write it up. […]

Leave a Reply