I suppose everyone can recall blog posts about building your own blog in 15 minutes with Ruby on Rails. Building a simple blog post page with Rails was as easy as hello world
I would like to encourage you to go through the following Elixir tutorial and the Phoenix web framework tutorial as well, or checking out a Youtube tutorial by Tensor Programming:
And for those old-school folks who still fancy reading, I recommend 'Programming Elixir' by Dave Thomas.
The main aim of this article is to draw your attention to Elixir and the Phoenix on the whole. I'm not going to provide you with a short tutorial and deep explanation of how it works. I would just like to show you how we can write something as simple as a blog with Elixir and the Phoenix framework.
Preparations — install Elixir and Phoenix
First of all, you will need to install Elixir. Next, install Hex package manager and Phoenix:
$ mix local.hex
$ mix archive.install
https://github.com/phoenixframework/phoenix/releases/download/v0.16.1/phoenix_new-0.16.1.ez
These steps are thoroughly described here.
Step 1 — create a project, dependencies, and compile
Let's create a project named 'blog_phoenix' using:
$ mix phoenix.new blog_phoenix
You can see that the following files were created:
* creating blog_phoenix/config/config.exs
* creating blog_phoenix/config/dev.exs
* creating blog_phoenix/config/prod.exs
* creating blog_phoenix/config/prod.secret.exs
* creating blog_phoenix/config/test.exs
* creating blog_phoenix/lib/blog_phoenix.ex
* creating blog_phoenix/lib/blog_phoenix/endpoint.ex
* creating blog_phoenix/test/controllers/page_controller_test.exs
* creating blog_phoenix/test/views/error_view_test.exs
* creating blog_phoenix/test/views/page_view_test.exs
* creating blog_phoenix/test/views/layout_view_test.exs
* creating blog_phoenix/test/support/conn_case.ex
* creating blog_phoenix/test/support/channel_case.ex
* creating blog_phoenix/test/test_helper.exs
* creating blog_phoenix/web/channels/user_socket.ex
* creating blog_phoenix/web/controllers/page_controller.ex
* creating blog_phoenix/web/templates/layout/app.html.eex
* creating blog_phoenix/web/templates/page/index.html.eex
* creating blog_phoenix/web/views/error_view.ex
* creating blog_phoenix/web/views/layout_view.ex
* creating blog_phoenix/web/views/page_view.ex
* creating blog_phoenix/web/router.ex
* creating blog_phoenix/web/web.ex
* creating blog_phoenix/mix.exs
* creating blog_phoenix/README.md
* creating blog_phoenix/lib/blog_phoenix/repo.ex
* creating blog_phoenix/test/support/model_case.ex
* creating blog_phoenix/priv/repo/seeds.exs
* creating blog_phoenix/.gitignore
* creating blog_phoenix/brunch-config.js
* creating blog_phoenix/package.json
* creating blog_phoenix/web/static/css/app.scss
* creating blog_phoenix/web/static/js/app.js
* creating blog_phoenix/web/static/assets/robots.txt
* creating blog_phoenix/web/static/vendor/phoenix.js
* creating blog_phoenix/web/static/assets/images/phoenix.png
* creating blog_phoenix/web/static/assets/images/favicon.ico
Then we need to install dependencies by running:
$ cd blog_phoenix
$ mix deps.get
You can notice that mix
$ mix phoenix.server
After compiling, we have a running app http://localhost:4000
Step 2 — create a table for posts
Now we are ready to start writing our core functionality. We would like to be able to have CRUD actions for posts and also to have the ability to add comments to each post (as it was decided at the beginning - just a simple blog post application). In order to achieve these goals, Phoenix supports us with 4 kinds of generators:
$ mix phoenix.gen.html → which creates: model, view, controllers, repository, templates, tests
$ mix phoenix.gen.channel → which creates: channel and tests
$ mix phoenix.gen.json → for API, which creates: model, view, controllers, repository, tests
$ mix phoenix.gen.model → which creates: model and repository
We use the first generator which creates all resources and actions for us - the same as rails generators. We need to declare the name in singular and plural, and
$ mix phoenix.gen.html Post posts title:string body:text
Now the CRUD actions for Post are ready.
The following files have been created:
* creating priv/repo/migrations/20150730233126_create_post.exs
* creating web/models/post.ex
* creating test/models/post_test.exs
* creating web/controllers/post_controller.ex
* creating web/templates/post/edit.html.eex
* creating web/templates/post/form.html.eex
* creating web/templates/post/index.html.eex
* creating web/templates/post/new.html.eex
* creating web/templates/post/show.html.eex
* creating web/views/post_view.ex
* creating test/controllers/post_controller_test.exs
Before we refresh the browser, however, we need to add a new endpoint web/router.ex
resources "/posts", PostController
To see our routing list we can use:
$ mix phoenix.routes
which is also similar to the one from Rails world. Phoenix uses Ecto by default to communicate and interact with the database. Ecto provides us with adapters to PostgreSQL, MySQL and SQLite (the number of databases supported is still growing). I'm not going too deep, but you can find a good description of Ecto library under Phoenix documentation or on GitHub.
Ecto allows us to create a proper post table in our database by running a migration. To see the application migration files, we need to go to priv/repo/migrations/
and run the migration by
$ mix ecto.migrate
but then an error occurs: we didn't create our database, so we have to create a project database using
$ mix ecto.create
$ mix ecto.migrate
You might notice that mix ecto.something
rake db:something
Under http://localhost:4000/posts
you can see CRUD functions in action which were generated. Feel free to play around with it.
Step 3 — create a table for comments
Finally, let's write some real code... The next step in our blog application is to enable post comments, namely to obtain the ability to see current post comments and add new ones. Let’s assume that we would like to have many comments for each blog post. Let’s use another generator to create a Comment
model and migration by model generator:
$ mix phoenix.gen.model Comment comments name:string content:text post_id:references:posts
For defining associations, as you can see, we Comment
model:
defmodule BlogPhoenix.Comment do
use BlogPhoenix.Web, :model
schema "comments" do
field :name, :string
field :content, :string
belongs_to :post, BlogPhoenix.Post, foreign_key: :post_id
timestamps
end
@required_fields ~w(name content post_id)
@optional_fields ~w()
@doc """
Creates a changeset based on the `model` and `params`.
If `params` are nil, an invalid changeset is returned
with no validation performed.
"""
def changeset(model, params \\ :empty) do
model
|> cast(params, @required_fields, @optional_fields)
end
end
The other side of our associations is that web/models/post.ex
and add: has_many :comments, BlogPhoenix.Comment
defmodule BlogPhoenix.Post do
use BlogPhoenix.Web, :model
schema "posts" do
field :title, :string
field :body, :string
has_many :comments, BlogPhoenix.Comment
timestamps
end
end
Model and migration files were created, so we can run migration:
$ mix ecto.migrate
Now that we have a comments table in our database, we need to add the add_comment action to routing and write a proper function in PostControler add_comment/2
resources "/posts", PostController do
post "/comment", PostController, :add_comment
end
We have just nested add_comment /posts
$ mix phoenix.routes
post_post_path POST /posts/:post_id/comment BlogPhoenix.PostController :add_comment
Next, let's make changes to the PostController. We want to have easy access
alias BlogPhoenix.Comment
Read more about aliases.
plug :scrub_params, "comment" when action in [:add_comment]
And define add_comment
def add_comment(conn, %{"comment" => comment_params, "post_id" => post_id}) do
changeset = Comment.changeset(%Comment{}, Map.put(comment_params, "post_id", post_id))
post = Post |> Repo.get(post_id) |> Repo.preload([:comments])
if changeset.valid? do
Repo.insert(changeset)
conn
|> put_flash(:info, "Comment added.")
|> redirect(to: post_path(conn, :show, post))
else
render(conn, "show.html", post: post, changeset: changeset)
end
end
The changeset function is defined web/model/comment.ex
We have changed the show function preloading post comments and added a Comment changeset because we would like to have a comment form in the post view:
def show(conn, %{"id" => id}) do
post = Repo.get(Post, id) |> Repo.preload([:comments])
changeset = Comment.changeset(%Comment{})
render(conn, "show.html", post: post, changeset: changeset)
end
We create a comment from web/templates/post/comment_form.html.eex
<%= form_for @changeset, @action, fn f -> %>
<%= if f.errors != [] do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below:</p>
<ul>
<%= for {attr, message} <- f.errors do %>
<li><%= humanize(attr) %> <%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<label>Name</label>
<%= text_input f, :name, class: "form-control" %>
</div>
<div class="form-group">
<label>Content</label>
<%= textarea f, :content, class: "form-control" %>
</div>
<div class="form-group">
<%= submit "Add comment", class: "btn btn-primary" %>
</div>
<% end %>
And render that template in web/templates/post/show.html.eex
<%= render "comment_form.html", post: @post, changeset: @changeset,
action: post_post_path(@conn, :add_comment, @post) %>
Step 4 — display the author and content of a comment
Now we can add new comments to our post, but we still can’t see the results of this action. Since we want to see all comments added to the current post, we need to create a new partial web/templates/post/comments.html.eex
and inside it, we iterate through all post comments and display the author of the comment and content. We have already preloaded the comments
<h3> Comments: </h3>
<table class="table">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<%= for comment <- @post.comments do %>
<tr>
<td><%= comment.name %></td>
<td><%= comment.content %></td>
</tr>
<% end %>
</tbody>
</table>
Additionally, we need to render that template in web/templates/post/show.html.eex
<%= render "comments.html", post: @post %>
Step 5 — show number of comments
The last functionality needed is to show the number of comments next to the list of our blog posts. We need to create a query where we can count the number of comments. We can do this inside the web/models/post.ex
count_comments
Posts
count
defmodule BlogPhoenix.Post do
use BlogPhoenix.Web, :model
import Ecto.Query
...
def count_comments(query) do
from p in query,
group_by: p.id,
left_join: c in assoc(p, :comments),
select: {p, count(c.id)}
end
end
In web/controllers/post_controller.ex
inside the index
function, we need to use the count_comments
function from above:
def index(conn, _params) do
posts = Post
|> Post.count_comments
|> Repo.all
render(conn, "index.html", posts: posts)
end
We modified the Posts
collection structure a bit, so we need to apply some changes to the template: web/templates/post/index.html.eex
:
<h2>Listing posts</h2>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Comments</th>
<th></th>
</tr>
</thead>
<tbody>
<%= for {post, count} <- @posts do %>
<tr>
<td><%= post.title %></td>
<td><%= count %></td>
<td class="text-right">
<%= link "Show", to: post_path(@conn, :show, post), class: "btn btn-default btn-xs" %>
<%= link "Edit", to: post_path(@conn, :edit, post), class: "btn btn-default btn-xs" %>
<%= link "Delete", to: post_path(@conn, :delete, post), method: :delete, class: "btn btn-danger btn-xs" %>
</td>
</tr>
<% end %>
</tbody>
</table>
Let's see how our blog post application works: http://localhost:4000/posts
Summary:
The source code for this application is stored on GitHub. This Blog application is very simple and it's just an attempt to prove how easy it is to play with Elixir and the Phoenix framework when we have a Ruby and Rails background. As you can see, it's as easy and fun as it was with Rails! I can remember I felt the same enthusiasm when I first met Ruby :) Is this, perhaps, something you call love at first sight?
We can see many similarities between Elixir on Phoenix and Ruby on Rails as far as conventions are concerned. This framework offers many familiar concepts like models, routing, controllers, and form helpers, but also some new approaches like repositories, changesets and channels. Because of that, we feel much more comfortable writing the code, but we have to remember that it's not an Object-oriented style of programming anymore. Therefore, we need to set our minds to ‘functional programming mode’ - and it’s equally as exciting!
I hope this tutorial encourages you to take a closer look
Looking for Elixir developers?
Our devs are so communicative and diligent you’ll feel they are your in-house team. Work with JavaScript experts who will push hard to understand your business and squeeze the most out of Elixir.