Check out the Alchemist's Edition

#52: Exploring Phoenix Assigns

Elixir 1.6

Phoenix 1.3

View source on GitHub


If you’re learning Phoenix you may have found yourself wondering how you set and access shared data you need through the lifecycle of a connection. In this episode we’ll learn about ‘assigns’ and how we can use it to access shared data in different contexts. There are a few different kinds of ‘assigns’: connection assigns, template assigns, and socket assigns.

Let’s first look at the the ‘assigns’ that are part of the connection. On the Plug.Conn there’s a field on the named ‘assigns’. This is where you can put shared data that needs to be used by different plugs throughout the lifecycle of a connection. Perhaps the most common type of assigns you’ll interact with are the template assigns. Before we look at how they work, let’s look at a Phoenix application we have running.

It’s a simple record store app that lists a few different albums. We can also like an album, which will trigger an alert with the name of the album we liked. We also link to an album’s show page that has a few more details about an album.

Now all the albums displayed here are part of the template assigns. Let’s take a look at how this is done.

We’ll open our album_controller.ex and we’ll go to our index action - this is the action that rendered the page we just saw.

We’re getting all of our albums with the Records.list_albums() then we’re rendering our template by calling the render function. We pass it the conn, the name of the template we want to render - index.html. Then we are passing in some data. Specifically we are passing in our albums under the key albums. These are our template assigns.

These are merged with any assigns present in the Plug.Conn assigns field and then accessible from our template as a variable named ‘assigns’.

Let’s look at what that means. We’ll open the index.html template we’re rendering here. And add an IEx.pry() line, which will allow us to pry into the process.

Template path: lib/teacher_web/templates/album/index.html.eex

<% require IEx; IEx.pry() %>
...

Then we’ll need to re-start our server with:

$ iex -S mix phx.server

And refresh our page to trigger our IEx.pry() line. Then we’ll allow it.

And I’ll clear the screen here for some more real estate. Now we can inspect some details of our process. So let’s take a look at what exactly is in our assigns. Since our assigns is a map, we’ll use Map.keys to list the keys of the different assigns.

And we see that we have our albums, the connection, view_module, and view_template. Our view module is our AlbumView Our view template is index.html. These ‘assigns’ are special in that they’re reserved by Phoenix. Then our albums are just a list of album structs. These are what is being displayed in on our page.

> Map.keys(assigns)
[:albums, :conn, :view_module, :view_template]
> assigns[:view_module]
TeacherWeb.AlbumView
> assigns[:view_template]
"index.html"
> assigns[:albums]
[
  %Teacher.Records.Album{
  ...
]

Now if we go back to our template we see that we’re actually accessing our albums with @albums. This is nice convenience Phoenix gives us, the @ is a macro that essentially calls Map.get to get our given key in the template assigns.

We can also see this in the app.html.eex. Our different templates are actually being rendered using the @view_module and @view_template in our template assigns. We easily can set additional data to our template assigns, let’s go back to the album_controller.ex and update the assigns to include some more data.

Let’s say we want to render a “Built with Elixir and Phoenix” message at the bottom of our page. We’ll create message, and include it with our assigns.

lib/teacher_web/controller/album_controller.ex

...
def index(conn, _params) do
  albums = Records.list_albums()
  built_with = "Built with Elixir and Phoenix"
  render(conn, "index.html", albums: albums, built_with: built_with)
end
...

And then let’s update our app.html.eex template and include our message.

Template path: lib/teacher_web/templates/layout/app.html.eex

    ...
    <p class="pull-center"><%= @built_with %></p>
    ...

Now if we go back to our browser we can see it being displayed on our page. One thing to remember when accessing template assigns it like this, we need to make sure the assigns will always be there. For example if we were to go to a page where it’s not being set, like the show page, we’ll get an error.

In instances where we want to access a value from the assigns that’s not guaranteed to be there, we can access it with the square bracket syntax, which will return nil if the key is not present instead of raising an exception.

Template path: lib/teacher_web/templates/layout/app.html.eex

    ...
    <p class="pull-center"><%= assigns[:built_with] %></p>
    ...

Now let’s take a look at another way ‘assigns’ are used - with the socket. And to understand how we can use them, let’s actually implement a simple feature in our application.

Remember the album’s “like” link from earlier? Currently if we like an album, an alert is triggered telling us the name of the album we like. Let’s update it so that when we like an album, it’s displayed at the top of the page. We’ll also have it track all the albums we like.

Phoenix.Socket fields. Just like in our Plug.Conn the assigns are stored in a field named ‘assigns’.

We’ll be tracking the albums liked by storing them in our Socket assigns. Let’s start building this feature by creating a new channel.

We’ll go to the command line generate a new channel named room:

$ mix phx.gen.channel room
* creating lib/teacher_web/channels/room_channel.ex
* creating test/teacher_web/channels/room_channel_test.exs

Add the channel to your `lib/teacher_web/channels/user_socket.ex` handler, for example:

    channel "room:lobby", TeacherWeb.RoomChannel

Then we’ll open our user_socket.ex and include our new RoomChannel

lib/teacher_web/channels/user_socket.ex

...
channel "room:*", TeacherWeb.RoomChannel
...

Now let’s open our room_channel.ex and simplify it by removing the authentication logic from our join callback.

We’ll remove the handle_in “ping” callback, and the authorized? function. Now what we’re left with is our join callback for when a user joins our room:lobby and our handle_in “shout” callback.

lib/teacher_web/channels/room_channel.ex

defmodule TeacherWeb.RoomChannel do
  use TeacherWeb, :channel

  def join("room:lobby", payload, socket) do
    {:ok, socket}
  end

  # It is also common to receive messages from the client and
  # broadcast to everyone in the current topic (room:lobby).
  def handle_in("shout", payload, socket) do
    broadcast socket, "shout", payload
    {:noreply, socket}
  end
end

Let’s update our join callback to include some initial state in the socket assigns. We’ll give it two assigns - one a message that we’ll include with the albums we liked. The other will be the list of liked albums.

In order to update the assigns in our socket, let’s use the Socket’s assign function to add key/value pairs.

Let’s create some messages we can display as a module attribute named @messages. Then let’s take our socket and pipe it into the assign function. We’ll use the key ‘message’, and we’ll set it with a random message. Then we’ll pipe it into assign again for our albums, which will start as an empty list.

lib/teacher_web/channels/room_channel.ex

defmodule TeacherWeb.RoomChannel do
  use TeacherWeb, :channel

  @messages ["You liked: ", "Thanks for liking: "]

  def join("room:lobby", payload, socket) do
    socket = socket
      |> assign(:message, Enum.random(@messages))
      |> assign(:albums, [])
    {:ok, socket}
  end
  ...
end

Now let’s go to app.js and let’s get our channel. Then we can join it. Now where we were just calling alert, let’s change that to push our “shout” event to the server with the name of our album.

assets/js/app.js

...
let channel = socket.channel("room:lobby", {});
let likeLinks = document.getElementsByClassName("like-album");
let messageElem = document.getElementById("album-like-message");

channel.join()

for (let link of likeLinks ) {
  link.addEventListener("click", function(e) {
    channel.push("shout", {album: link.dataset.album});
  }, false);
}

These events will be handled by the handle_in “shout” callback on our room_channel.ex. So let’s open that. We’ll grab the album we liked from the payload.

Then we’ll call the assign function and we’ll update our existing albums with our new album. Then we’ll broadcast a “shout” event back to our client, giving it our assigns.

lib/teacher_web/channels/room_channel.ex

...
def handle_in("shout", payload, socket) do
  new_album = payload["album"]
  socket = assign(socket, :albums, socket.assigns.albums ++ [new_album])
  broadcast socket, "shout", socket.assigns
  {:noreply, socket}
end
...

Now let’s handle this event on the client. We’ll go back to app.js and we’ll add channel.on “shout”. Then we’ll get our albums from the payload and join them. And we’ll use update our messageElem to display our message and our albums.

assets/js/app.js

...
channel.on("shout", payload => {
  let albums = payload.albums.join(", ")
  messageElem.innerText = `${payload.message} ${albums}`
})

Then we’ll start our server again:

$ mix phx.server

Now if we go back to our browser and like an album, instead of an alert being triggered, it’s added to our page along with a message. Then if we like multiple albums, they’re appended.

Because our message is set initially, we use the same one. But if we try reloading the page. And the like an album - our other message is displayed.