Building a Strawpoll clone in Phoenix with channels

Current Versions

Elixir: v.1.1.0

Phoenix: v1.0.3

Ecto: v1.0.6


She’s probably voting on that computer! Or maybe not. It’s very hard to find good stock photos for live voting apps.

Introduction

One of the things that interested me the most about Phoenix and Elixir is the capabilities that are available for real-time data synchronization and broadcasting with things like Websockets. It has become such an alluring part of Phoenix that chat applications have affectionately become Phoenix’s “hello world” example, even! The reason for this is two-fold: channels are a first-class citizen in Phoenix, so adding them does not require working AROUND the framework, and they’re so simple to add that knowledge isn’t a barrier to implementation either. Based on that, let’s implement our own coming-of-age application in Phoenix too!

What is Strawpoll?

Strawpoll is a realtime polling platform written in nodejs that also takes advantage of websockets. As each vote comes in, the vote is broadcast to everyone who has that poll window open and has voted. So we have a few real time concepts to deal with: receiving broadcasts when someone else votes and sending out broadcasts when we vote, as well as how the polls are modified when they are live (such as closing a poll). It also displays/updates a nice little pie chart on the side of the app in realtime, so we’ll do that too!

Getting started

We need to start with building our Phoenix application, which is a pretty standard and simple process:

mix phoenix.new haypoll
cd haypoll
mix ecto.create
iex -S mix phoenix.server

If everything worked, you should be able to visit http://localhost:4000 and see the standard Phoenix “Getting Started” page!

Setting up our data model

Let’s discuss what we will need from a data perspective. We want to make sure we’re persisting these polls and votes. For our polls, all we really need is the name of the poll and whether it’s still open for voting or not.

> mix phoenix.gen.html Poll polls title:string closed:boolean

As the command states, we need to modify web/router.ex to add the new resource path:

web/router.ex:

scope “/”, Haypoll do
pipe_through :browser # Use the default browser stack
  get “/”, PageController, :index
resources “/polls”, PollController
end

Finally, we’ll cap it off by running mix ecto.migrate and make sure that our new table is added with the appropriate columns! Now, let’s create our concept of entries:

> mix phoenix.gen.model Entry entries title:string votes:integer poll_id:references:polls

We don’t want to create full templates and controllers and everything for entries since they don’t really exist much on their own; they really just exist for the purpose of serving data for the polls. We also set up a reference relationship between polls and entries. Once again, run mix ecto.migrate and verify the results. This will set up the “belongs_to” relationship on entries back to polls, but not the other way around. Let’s do that really quick. In web/models/poll.ex:

schema “polls” do
field :title, :string
field :closed, :boolean, default: false
  has_many :entries, Haypoll.Entry, on_delete: :delete_all
  timestamps
end

Not only do we specify the “entries” association, but we also specify some behavior for when a poll gets deleted that will delete all of the entries that were attached to that poll.

We also need to modify our web/models/entry.ex model to specify a default value for the number of votes to start at 0.

schema “entries” do
field :title, :string
field :votes, :integer, default: 0
belongs_to :poll, Haypoll.Poll
  timestamps
end

Finally, restart your server that’s running via iex to make sure it grabs these changes.

Making our poll creation a little more dynamic

Right now our poll creation page is a little static, and there’s no way for us to modify the list of entries available to our poll.

Our default form for Polls

This is a good first start but we can improve it! Let’s make it so that we can add/remove entries from this page and create the poll and associated entries. There’s going to be a lot of javascript work involved here, so we’ll start with our list of dependencies.

Preparing our javascript dependencies

To make this pretty simple, we’ll just use jquery to modify our dom on the fly. To get jquery installed on a global level, we’ll just use a CDN to load the jquery library, and we’ll make it accessible to our entire application via our application layout. Open up web/templates/layout/app.html.eex and add the following:

  </div> <! -- /container -->
<script src=”//code.jquery.com/jquery-1.11.3.min.js”></script>
<script src=”<%= static_path(@conn, “/js/app.js”) %>”></script>

Next, we’ll need to add a simple list of entries and a way to add a new entry to our poll form. In web/templates/poll/form.html.eex:

web/templates/poll/form.html.eex

We’ve added a new section with a header of Entries and a button to add more entries. We’ve also added our initial template with a text input for creating a new entry with a particular title.

Our updated form with multiple entry addition

It’s not super pretty, but that’s okay! We can always clean up a UI later (or pay someone else to do it!). Now, what we want to happen is that every time someone clicks “Add Entry”, the top entry gets copied with a blank value, and if someone clicks the “remove” button next to an entry, it removes that particular row. To do this we’re going to have to write some javascript code. Let’s create a new file that will serve as our workhorse for the Polls page.

Create web/static/js/poll.js:

web/static/js/poll.js

I’ve commented each line so you can follow along with the javascript code. And then in web/static/js/app.js add the following to the bottom:

import { Poll } from “./poll”
let poll = new Poll()

And clean up the display with some css by adding this to the bottom of web/static/css/app.css:

.entry {
margin-top: 20px;
margin-bottom: 20px;
}

This will spread the entries apart a little bit and it will look ever so slightly better. Now, we need to modify the create action in web/controllers/poll_controller.ex to create the entries with the poll. Start off by adding a line to alias the Entry model with alias Haypoll.Entry:

web/controllers/poll_controller.ex

We have to create a new function to delegate the work and clean up our codebase a little bit, so let’s take a look at line 12 above, the create_poll function. We start off with a call to:

Repo.transaction fn -> ...

This is going to wrap our operation inside of a transaction. If we fail to create an entry or the poll, we want to make sure the whole thing gets rolled back. Next, we just create our standard Poll changeset; nothing exciting here.

We then match on our case for a successful poll insert with the {:ok, poll} tuple. Next, we use Enum.map to call an anonymous function that will build the associated entry for the poll and then insert it. We’re expecting that an errors should fail us out of our transaction, so we use Repo.insert! instead of the standard insert function.

We also match on our case for the {:error, changeset}, where if we fail to insert, we force the transaction to rollback on the changeset. In our create function, we case on the create_poll function instead of our standard Repo.insert. This just makes our logic a little easier to follow and keeps our function a little smaller.

We can create a poll, with entries, but we can’t actually verify that the entries have been created yet. We’ll fix that, however, by modifying our poll show template. Open up web/templates/poll/show.html.eex:

web/templates/poll/show.html.eex

Notice that we also added a hidden input at the top with an id of “poll-id” and a value of the current poll’s primary key. We will need that later! We also add a “data-entry-id” attribute to the list item with a value of the current entry’s id, and wrap the number of votes inside of its own span. We finally add a “vote” class to the Vote button. Save this file now.

Unfortunately, we’re getting a compilation error here about:

protocol Enumerable not implemented for #Ecto.Association.NotLoaded<association :entries is not loaded>

The reason for this is that Ecto will not allow you to shoot yourself in the foot with associations running N+1 queries all over the place. We need to explicitly tell Ecto that we need to preload any associations, so let’s head back to web/controllers/poll_controller.ex and modify the show function:

web/controllers/poll_controller.ex

We define a query to use to fetch our entries which orders by the entry’s primary key. We then feed that query into the poll query, which preloads associated entries via the entry query we wrote. Finally, we use that combined query to fetch a specific poll with a specific id, and feed that all to the show template via our assigns.

Our UI with voting buttons!

Now that we have our UI for polls, it’s time to take it one step further. We’re now going to implement live voting via channels!

Creating and joining a channel

We need to start off by creating the actual channel. We start off working with a couple of common setup details. First, we open up web/channels/user_socket.ex to set up our poll channel and tell Phoenix where to route socket requests/responses through. In web/channels/user_socket.ex, add the following:

channel “polls:*”, Haypoll.PollChannel

And now we need to create our PollChannel module. Open up web/channels/poll_channel.ex:

defmodule Haypoll.PollChannel do
use Phoenix.Channel
  def join(“polls:” <> poll_id, _params, socket) do
{:ok, socket}
end
end

We have to set up a “join” function that pattern matches on which channel the user is joining. In our case, we’re creating a channel for each individual poll, so we match on “polls:” <> poll_id. This line is pretty neat; what we’re doing here is pattern matching on a part of a string. We’re expecting the string to start with “polls:”, and then anything following that we’re assigning to the “poll_id” variable. We’re ignoring the params sent in, since we’re not going to use them for anything. Finally, we accept in the socket, since we need to turn around and respond with a tuple of {:ok, socket}.

Next, we need to write some javascript code to demonstrate that we can, at least, join this channel. We’ll save the actual interaction and broadcasting pieces for later. Open up web/static/js/live_poller.js and let’s begin. We’re going to start off relatively simple but build this up later:

web/static/js/live_poller.js

Again, I’ve commented all of the code to make it easier to follow along. When we open up the poll show page, we should now expect to see “Connected” and “Joined” show up in our javascript log, and we should not expect to see that on any of the other pages! Now we’re ready to start broadcasting and dealing with messages! Finally, open up web/static/js/app.js and add the following at the bottom:

import { LivePoller } from "./live_poller"
let livePoller = new LivePoller()

Pushing and broadcasting messages

Next, we need to be able to broadcast out when a user votes (and receive the vote as well). We can deal with this via the handle_in function in our PollChannel module.

web/channels/poll_channel.ex

Here, we’re expecting a broadcast with a message of “new_vote” and expecting to be sent entry_id as one of the parameters. We also are required to pass in the socket. We then fetch the appropriate Entry by the entry_id and then create a changeset that increments the number of votes for that entry by 1. Then, we attempt to update that entry in the database.

If the update was a success, we broadcast out to the socket with a “new_vote” message, supplying an entry_id matching the updated entry’s primary key, and then just respond out with {:noreply, socket}.

If it fails, we send out an error response with a map that includes the reason. If you look back to the javascript code:

.receive(“error”, reason => { console.log(“Error: “, reason) })

Notice that we’re expecting an “error” response (like the :error part of our tuple) and a “reason” in the map for our response! When you tease all of this apart, the whole channel concept really simplifies itself! Now, we have to finish up our javascript code. In setupPollChannel(), after we define our pollChannel, we need to create a new event handler to deal with new_vote messages:

// Set up a handler for when the channel receives a new_vote message
pollChannel.on(“new_vote”, vote => {
// Update the voted item’s display
this._updateDisplay(vote.entry_id)
})

Notice there’s a new function call here, self.updateDisplay. We haven’t written that yet, so let’s do so:

web/static/js/live_poller.js: updateDisplay()

We also reference an “updateEntry” function that we need to define, so let’s do that next:

_updateEntry(li, newVotes) {
// Find the .votes span and update it to whatever the new votes value is
li.find(“.votes”).text(newVotes)
}

Simple function; this is pretty much all just gruntwork to support updating the display and making the UI buttons do things. Finally, we need to write a function that will actually set up the vote buttons and hook them into broadcasting our message. We’ll define one more function: setupVoteButtons.

web/static/js/live_poller.js: setupVoteButtons()

The only part here that even pertains to the channels is that “pollChannel.push(…)” function call. It pushes a message, in our case “new_vote”, out to the channel with a list of parameters. In our case, the only parameter we actually care about is entry_id! The final contents of our web/static/js/live_poller.js file should look like the following:

web/static/js/live_poller.js

Your project is now functional! Not pretty, but functional! You should have live-updating voting numbers, and if you open up multiple browsers, each of your browsers will receive the results live as you go along! And we didn’t even write all that much just to make this project work! Let’s just add one final bit of shiny “interfaceness” to make this project a little more like Strawpoll.

Adding in a live-updating graph

Let’s take it one step further and add in a graph to track all of our votes. For this, I’ll just stick to Google’s graphing/visualization libraries in javascript as they’re pretty simple to understand and implement. I’ll leave it up to you if you want to roll with something a bit different! Let’s start by changing our Poll show template. Wrap the ul inside of a div with a class of “col-xs-8”, and then add another div below all of that with a class of “col-xs-4” and an ID of “my-chart”. We’ll also add a script tag referencing the google jsapi codebase:

<div class=”col-xs-4">
<div id="my-chart"></div>
</div>
<script type=”text/javascript” src=”https://www.google.com/jsapi"></script>

Also, update the <%= entry.title %> line to be wrapped inside of a div with the class of “title”.

<span class=”title”><%= entry.title %></span>

Next, update web/static/css/app.css and add an entry for #my-chart to make it a particular size:

#my-chart {
width: 400px;
height: 400px;
}

Finally, we need to go digging into our live_poller.js code a little more. We’ll start by adding some more code to handle our graph:

web/static/js/live_poller.js: graphing functions

Next, at the top of our LivePoller object, add the following line:

chart: null,

And add a call to setupGraph inside the init() function:

// And setup our graph
this._setupGraph()

And in our setupPollChannel function, where you build out the “new_vote” message handler, add the following lines:

// And update the graph, since we have new data
this._updateGraph()

Everything is now complete and we should have a nice new displayed graph that live updates per each vote!

Our new UI, complete with a fancy 3D pie chart!

Our final operation is to allow someone to close/reopen polls and broadcast the event appropriately.

Closing and opening polls

In our update function in web/controllers/poll_controller.ex we need to create a new broadcast when a poll is opened or closed!

Haypoll.Endpoint.broadcast(“polls:#{id}”, “close”, %{closed: poll.closed})

Let’s also add a CSS class for something that should be hidden:

.hidden {
display: none;
}

Next, let’s open up the setupPollChannel function in live_poller.js and add a new handler for the “close” message:

// Set up a handler for when the channel receives a close message
pollChannel.on("close", status => {
if (status.closed) {
$("a.vote").addClass("hidden")
$("#poll-closed").text("false")
} else {
$("a.vote").removeClass("hidden")
$("#poll-closed").text("true")
}
})

And finally, update web/templates/poll/show.html.eex to be able to respond to all of this new data:

web/templates/poll/show.html.eex

Our UI should now look like this when someone closes our poll:

Our updated UI for a closed poll

Conclusion

We now have a pretty awesome little live-updating live-polling application, and we really haven’t written that much new code, and certainly not complicated polling or socket code. Our application has a very good foundation now, but there are definitely still a few bugs to work out and there are zero tests for this so far! Certainly not ideal, but a great starting point. Here are some of the known bugs:

  1. We don’t list the entries at all on the Poll edit page
  2. Anyone can edit/delete any poll
  3. No tests whatsoever
  4. The UI/design kind of stinks

If I were to go in and implement all of these, this long post would get much, much longer, so instead I’m going to leave these as exercises to you, intrepid readers! If you want to take a look at the code generated so far, you can grab it from https://github.com/Diamond/haypoll_example.