In our last article, we transplanted the front-end of a simple Meteor example application into a Phoenix project and wired up a Blaze template to use Phoenix Channels rather than DDP.

Today we’ll be finishing our Franken-stack by replacing the hard-coded data we’re sending down to our clients with data persisted in a database. We’ll also implement the “Add Points” functionality using Channel events.

Let’s get to it!

Creating A Player Model

Before we start pulling data from a database, we need to lay some groundwork. We’ll be using Ecto to create a model of our player, and creating some seed data to initially populate our database.

In our Phoenix project directory, we’ll use mix to generate a new model for us:


mix phoenix.gen.model Player players name:string score:integer

The phoenix.gen.modal mix task will create both a PhoenixLeaderboard.Player model, and a migration file for us. The migration file will create our players table in PostgreSQL (Phoenix’s default database) when we run this command:


mix ecto.create

The out-of-the-box PhoenixLeaderboard.Player (web/models/player.ex) model is very close to what we want. It defines name as a string, score as an integer and a set of created/updated at timestamps.

The only change we need to make here is to specify how we want each Player to be serialized into JSON. We can do this by deriving the Poison.Encoder implementation:


defmodule PhoenixLeaderboard.Player do
  use PhoenixLeaderboard.Web, :model
  @derive { Poison.Encoder, only: [:id, :name, :score] }
  ...

Seeding Our Database

Now that we have a working Player model, let’s insert some seed data into our database.

By default, seeding a database in a Phoenix project is done by writing a script that manually inserts models into your repository. To insert all of our players, we could add the following to priv/repo/seeds.exs:


alias PhoenixLeaderboard.Repo
alias PhoenixLeaderboard.Player

Repo.insert! %Player{ name: "Ada Lovelace", score: 5 }
Repo.insert! %Player{ name: "Grace Hopper", score: 10 }
Repo.insert! %Player{ name: "Marie Curie", score: 15 }
Repo.insert! %Player{ name: "Carl Friedrich Gauss", score: 20 }
Repo.insert! %Player{ name: "Nikola Tesla", score: 25 }
Repo.insert! %Player{ name: "Claude Shannon", score: 30 }

We can run this seed script with the following mix task:


mix run priv/repo/seeds.exs

If all went well, all six of our players should be stored in our database!

Publishing Players

Let’s revisit the join function in our PhoenixLeaderboard.PlayersChannel (web/channels/players_channel.ex) module.

Last time, we simply returned a hard-coded list of cleaners whenever a client joined the "players" channel. Instead, let’s return all of the players stored in the database.

To shorten references, we’ll start by aliasing PhoenixLeaderboard.Repo and PhoenixLeaderboard.Player, just like we did in our seed file:


defmodule PhoenixLeaderboard.PlayersChannel do
  use Phoenix.Channel
  alias PhoenixLeaderboard.Repo
  alias PhoenixLeaderboard.Player
  ...

Now, refactoring our join function to return all players is as simple as calling Repo.all and passing in our Player model:


  def join("players", _message, socket) do
    {:ok, Repo.all(Player), socket}
  end

Looking back at our Leaderboard application, our player list should still be filled with our scientists.

Adding Points With Events

Now we get to the truly interesting part of this experiment.

In our original Meteor application, we updated each player’s score on the client and depended on DDP to propagate that change up to our server:


'click .inc': function () {
  Players.update(Session.get("selectedPlayer"), {$inc: {score: 5}});
}

Since we’re moving away from DDP, we can no longer rely on Meteor to do this for us. We’ll need to manage this update process ourselves.

Our plan for handling these updates is to push an "add_points" channel event up to the server whenever a user clicks on the .inc button:


Template.instance().channel.push("add_points", {
  id: Session.get("selectedPlayer")
});

In our PlayersChannel, we can handle any incoming "add_points" events using the handle_in function:


def handle_in("add_points", %{"id" => id}, socket) do
  player = Repo.get!(Player, id)
  Player.changeset(player, %{score: player.score + 5})
  |> Repo.update
  |> handle_player_update(socket)
end

Out logic here is fairly straightforward: get the Player with the given id, increment his score by 5, and then update the database with our changes.

The handle_player_update function handles the result of our Repo.update. If the update was successful, we’ll broadcast the "add_points" event down to all connected clients, passing the affected Player as the event’s payload:


defp handle_player_update({:ok, player}, socket) do
  broadcast! socket, "add_points", %{id: player.id, score: player.score}
  {:noreply, socket}
end

defp handle_player_update({:error, changeset}, socket) do
  {:reply, {:error, changeset}, socket}
end

The last piece of this puzzle is handling the "add_points" events we receive on the client. Every time we receive an "add_points" event from the server, we’ll want to update the provided Player in our Players Minimongo collection:


this.channel.on("add_points", (player) => {
  Players.update(player.id, {
    $set: {
      score: player.score
    }
  });
});

And that’s it!

Now if we navigate back to our Leaderboard application and start adding points to players, we’ll see their score and position change in the interface. If we connect multiple clients, we’ll see these changes in real-time as they happen.

Final Thoughts

As fun as this was, we don’t recommend you tear your Meteor application in half like we did. This was an experiment and a learning experience, not a production ready migration path.

In the future, we may investigate more reasonable and production ready migrations routes from an application built with Meteor to a Elixir/Phoenix environment. Stay tuned!

Lastly, we realized while building this Franken-stack that Meteor’s DDP and Phoenix Channels are not one-to-one replacements for each other. Try imagining how you would implement a Meteor-style pub/sub system in Channels. It’s an interesting problem, and one we’re excited to tackle in future posts.

If you want to run the Leaderboard yourself, check out the full project on GitHub. Feel free to open an issue if you have any questions, comments, or suggestions!