Using Elixir structs in JavaScript

Published January 19, 2019 by Toran Billups

Today I was building a Phoenix 1.4 application when I bumped into a problem. The trouble was that my exact error message didn't yield any clear results to help guide me so I decided to blog about my experience for anyone who might follow with a similar situation.

The problem

I wanted to take an Elixir struct and make it available to my JavaScript application. My first attempt looked something like this ...

    defmodule StructtWeb.PageController do
      use StructtWeb, :controller
    
      def generate_cards do
        [
          %StructtWeb.Card{id: 1, name: "bird", image: "/images/animals/bird.png"},
          %StructtWeb.Card{id: 2, name: "bird", image: "/images/animals/bird.png"},
          %StructtWeb.Card{id: 3, name: "cat", image: "/images/animals/cat.png"},
          %StructtWeb.Card{id: 4, name: "cat", image: "/images/animals/cat.png"},
        ]
      end
    
      def index(conn, _params) do
        cards = generate_cards()
        render(conn, "index.html", %{cards: cards})
      end
    end
  

The template for this controller made use of the `cards` struct by setting state on the VueJS component.

    new Vue({
      el: "#app",
      data: {
        cards: []
      },
      methods: {
        resetGame() {
          this.cards = <%= @cards %>;
        }
      },
      created() {
        this.resetGame();
      }
    });
  

When I started Phoenix from the command line using IEx I got the following error.

lists in Phoenix.HTML and templates may only contain integers representing bytes, binaries or other lists, got invalid entry

I did some searching around and it turns out you need to encode the data first before rendering the html. I stumbled upon Poison.encode for this type of thing, but before I added another dependency I did a little source diving and found that Phoenix provides a generic json_library I could use instead.

    defmodule StructtWeb.PageController do
      use StructtWeb, :controller
    
      def index(conn, _params) do
        {:ok, cards} =
          generate_cards()
          |> Phoenix.json_library().encode()
        render(conn, "index.html", %{cards: cards})
      end
    
    end
  

With the struct encoded now I got the following error message from Phoenix.

Jason.Encoder protocol must always be explicitly implemented.\n\nIf you own the struct, you can derive the implementation specifying which fields should be encoded to JSON...

So throwing a struct directly into the template won't work like I assumed it would. I was able to solve this by first transforming the struct into a map.

    defmodule StructtWeb.PageController do
      use StructtWeb, :controller
    
      def index(conn, _params) do
        {:ok, cards} =
          generate_cards()
          |> Enum.map(&Map.from_struct(&1))
          |> Phoenix.json_library().encode()
        render(conn, "index.html", %{cards: cards})
      end
    
    end
  

Phoenix was finally error free but I did see the JavaScript was heavily escaped so I had to use the `raw` keyword as shown below.

    new Vue({
      el: "#app",
      data: {
        cards: []
      },
      methods: {
        resetGame() {
          this.cards = <%= raw(@cards) %>;
        }
      },
      created() {
        this.resetGame();
      }
    });
  

You can find the source code for this example on github


Buy Me a Coffee

Twitter / Github / Email