We always have a hands on approach when learning new stuff and this article will help you learn the basics of Elixir and the Phoenix Framework by building a basic blog.
We'll asume you already have Elixir and Phoenix up and running.
Our purpose here is to create a simple CRUD app which will allow us to use basic data operations like save, edit and delete. Let's start by creating a simple blog with posts and comments.
#1 Setup our Elixir project
To start a project in Elixir, we use Mix, a build tool which provides everything necessary to create and manage Elixir apps. First, we need to use a mix task to create our project:
Create a new elixir project (1)
$ mix phx.new blog_app
The mix task creates our new blog_app template and files. You should see something like this:
Create a new elixir project (2)
* creating blog_app/config/config.exs * creating blog_app/config/dev.exs
* creating blog_app/config/prod.exs * creating blog_app/config/prod.secret.exs
* creating blog_app/config/test.exs
* creating blog_app/lib/blog_app/application.ex * creating blog_app/lib/blog_app.ex
...
* creating blog_app/assets/css/phoenix.css
* creating blog_app/assets/static/images/phoenix.png
* creating blog_app/assets/static/robots.txt
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix deps.compile
* running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
We're almost there! Let's move into the project's folder:
Create a new elixir project [3]
$ cd blog_app
Then configure your database in config/dev.exs and run:
Create a new elixir project (4)
$ mix ecto.create
Finally, start your Phoenix app with:
Create a new elixir project (5)
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
Create a new elixir project (6)
$ iex -S mix phx.server
If you chose "No" when asked to install dependencies, you can do it afterwards with the following mix command:
Install dependencies
$ mix deps.get
For the next step open up
config/dev.exs
in a text
editor and configure the database. Make sure that the username
and password match the ones you have set up on your local
machine. It should look like this:
Database configuration
config :blog_app, BlogApp.Repo,
username: "postgres",
password: "postgres",
database: "blog_app_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
As prompted earlier, we then need to
cd
into our newly created
project and run the
$ mix ecto.create
task to
create the database:
Create database
$ cd blog_app
$ blog_app mix ecto.create
Compiling 14 files (.ex) Generated blog_app app The database for BlogApp.Repo has been created
Let's see if everything works as it's supposed to. We'll start
up the server by running either
mix phx.server
or
iex -S mix phx.server
(IEx
stands for Interactive Elixir). Our application will be running
under
http://localhost:4000.
We should be presented with the "Welcome to Phoenix!" page:
#2 Create a Post resource
Phoenix provides an easy way to setup the basic resources we need through the use of generators - mix tasks which will build the respective modules. What we need is the phx.gen.html, which will generate the controller, views and context.
Fire up the terminal and enter this command:
Generate context, schema and database table
$ mix phx.gen.html Posts Post posts title:string body:text
What we did here, was creating the
Post
resource. The resource
belongs to the Posts
context
and has a posts
schema, with a
title
field of type string and
a body
field of type text.
The CRUD actions for our
Post
resource are now ready.
The following files have been created:
Creating controller, views and context files
$ mix phx.gen.html Posts Post posts title:string body:text
* creating lib/blog_app_web/controllers/post_controller.ex
* creating lib/blog_app_web/templates/post/edit.html.eex
* creating lib/blog_app_web/templates/post/form.html.eex
...
* creating lib/blog_app/posts.ex
* injecting lib/blog_app/posts.ex
* creating test/blog_app/posts_test.exs
* injecting test/blog_app/posts_test.exs Add the resource to your browser scope in lib/blog_app_web/router.ex:
resources "/posts", PostController
Remember to update your repository by running migrations:
$ mix ecto.migrate
If we take a look in the
lib/blog_app/posts/post.ex
file of our blog_app, we'll see the posts schema matching the
database table 'posts' we created.
lib/blog_app/posts/post.ex
defmodule BlogApp.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :body, :string
field :title, :string
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end
In
lib/blog_app_web/controllers/post_controller.ex
- you'll notice that we can access the Post resource via the
Posts
context we created.
lib/blog_app_web/controllers/post_controller.ex
defmodule BlogAppWeb.PostController do
use BlogAppWeb, :controller
alias BlogApp.Posts
alias BlogApp.Posts.Post
def index(conn, _params) do
posts = Posts.list_posts()
render(conn, "index.html", posts: posts)
end
def new(conn, _params) do
changeset = Posts.change_post(%Post{})
render(conn, "new.html", changeset: changeset)
end
...
end
In the
lib/blog_app/posts.ex
we can
see several functions that Phoenix implements by default.
lib/blog_app/posts.ex
def list_posts do
Repo.all(Post)
end
def get_post!(id), do: Repo.get!(Post, id)
def create_post(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
end
def delete_post(%Post{} = post) do
Repo.delete(post)
end
def change_post(%Post{} = post, attrs \\ %{}) do
Post.changeset(post, attrs)
end
Now let's open the
lib/blog_app_web/router.ex
module and define the route for our new posts resource.
Under the
get "/", PageController, :index
line, add
resources "/posts", PostController
.
lib/blog_app_web/router.ex
scope "/", BlogAppWeb do
pipe_through :browser
get "/", PageController, :index
resources "/posts", PostController
end
This has generated a new database migration, therefore we need
to run the task
mix ecto.migrate
to persist
the changes.
Now let's run our server again with
iex -S mix phx.server
and go
to
http://localhost:4000/posts
.
We'll see the Listing Posts page. Notice that we can do all the
basic CRUD operations with posts.
Using mix phx.routes
we can
inspect all the existing routes for our app. We can combine it
with grep,
mix phx.routes | grep posts
to
inspect only the posts routes.
Inspecting post routes
$ mix phx.routes | grep posts
post_path GET /posts BlogAppWeb.PostController :index
post_path GET /posts/:id/edit BlogAppWeb.PostController :edit
post_path GET /posts/new BlogAppWeb.PostController :new
post_path GET /posts/:id BlogAppWeb.PostController :show
post_path POST /posts BlogAppWeb.PostController :create
post_path PATCH /posts/:id BlogAppWeb.PostController :update
PUT /posts/:id BlogAppWeb.PostController :update
post_path DELETE /posts/:id BlogAppWeb.PostController :delete
#3 Add Comments to our Posts
Let's enable comments for our posts by using another Phoenix generator, phx.gen.context. This will create another context and the comments ecto schema:
Generating the comments context and schema
$ mix phx.gen.context Comments Comment comments name:string content:text post_id:references:posts
The
post_id:references:posts
is
the way you tell the generator to setup a relationship between
Post
and
Comment
. We'll see the
post_id
field added to the
comments
schema.
We're not finished yet, as we still need to define the association between the posts and the comments schemas ourselves.
We'll make use of the
Ecto.Schema
function has_many
for the
post.
lib/blog_app/posts/post.ex
defmodule BlogApp.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
alias BlogApp.Comments.Comment
schema "posts" do
field :body, :string
field :title, :string
has_many :comments, Comment
timestamps()
end
@doc false
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :body])
|> validate_required([:title, :body])
end
end
Here we used the
alias
directive to reference
the Comment like this:
has_many :comments, Comment
.
In the comment schema, we already have the
post_id
field added by the mix
task, so we only need to add the
post_id
to the changeset
below.
lib/blog_app/comments/comment.ex
defmodule BlogApp.Comments.Comment do
use Ecto.Schema
import Ecto.Changeset
schema "comments" do
field :content, :string
field :name, :string
field :post_id, :id
timestamps()
end
@doc false
def changeset(comment, attrs) do
comment
|> cast(attrs, [:name, :content, :post_id])
|> validate_required([:name, :content, :post_id])
end
end
Now we need to run the
mix ecto.migrate
task again.
After we're done with associations, we need to make it possible for the user to see and interact with post comments from the app interface.
In the router module we need to add another resource for the
comments, with the
add_comment
action and create
its corresponding function in PostControler.
router.ex
resources "/posts", PostController do
post "/comment", PostController, :add_comment
end
In the post_controller.ex
file
we'll add the function:
This will allow the creation of a new comment in the database and associate it to a post. It will also load the post page and show a flash message with the status of the create operation.
lib/blog_app_web/controllers/post_controller.ex
def add_comment(conn, %{"comment" => comment_params, "post_id" => post_id}) do
post =
post_id
|> Posts.get_post!()
|> Repo.preload([:comments])
case Posts.add_comment(post_id, comment_params) do
{:ok, _comment} ->
conn
|> put_flash(:info, "Added comment!")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, _error} ->
conn
|> put_flash(:error, "Oops! Couldn't add comment!")
|> redirect(to: Routes.post_path(conn, :show, post))
end
end
Make sure to alias the
BlogApp.Repo
at the beginning
of the post_controller file.
Now into the
/lib/blog_app/posts.ex
module
we'll add the
add_comment
action. Also
remember to
alias BlogApp.Comments
.
/lib/blog_app/posts.ex
def add_comment(post_id, comment_params) do
comment_params
|> Map.put("post_id", post_id)
|> Comments.create_comment()
end
We'll also need to create a simple form on the post's page in
order to allow users to comment. To achieve this, we'll update
the show action on the post controller. We need to preload
comments from the Repo and include a
Comment.changeset
into its
show.html
template, and alias
BlogApp.Comments.Comment
to be
able to use the
Comment.changeset
.
lib/blog_app_web/controllers/post_controller.ex
def show(conn, %{"id" => id}) do
post =
id
|> Posts.get_post!
|> Repo.preload([:comments])
changeset = Comment.changeset(%Comment{}, %{})
render(conn, "show.html", post: post, changeset: changeset)
end
After that's done, we'll create a
comment_form.html.eex
template:
lib/blog_app_web/templates/post/comment_form.html.eex
<%= form_for @changeset, @action, fn f -> %>
<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 %>
Now let's get to the
lib/blog_app_web/templates/post/show.html.eex
template and add in this line above the
edit
and
back
links:
lib/blog_app_web/templates/post/show.html.eex
<%= render "comment_form.html", post: @post, changeset: @changeset, action:
Routes.post_post_path(@conn, :add_comment, @post) %>
This will render our comment form on the post's page.
We can now add comments to our posts, but we can't see them yet. Let's display them in the blog pages.
#4 Displaying Post Comments
We'll need to create yet another template for our posts. Let's
call it
comments.html.eex
.
lib/blog_app_web/templates/post/comments.html.eex
<h3>Comments:</h3>
<div class="comments">
<div class="comment header">
<div>Name</div>
<div>Content</div>
</div>
<%= for comment <- @comments do %>
<div class="comment">
<div><%= comment.name %></div>
<div><%= comment.content%></div>
</div>
<% end %>
</div>
Then we'll render this new template on our posts page next to the comment_form.
lib/blog_app_web/templates/post/show.html.eex
<%= render "comments.html", comments: @post.comments %>
Add a little bit of styling in
app.scss
:
assets/css/app.scss
.comments {
padding-bottom: 2em;
}
.comment {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0.5em;
border-bottom: 1px solid lightgrey;
}
.comment.header {
font-weight: bold;
}
The comments are now displayed:
What if we would like to see the total number of comments for a post? Let's do that too:
We need to define a function to the get the number of posts in
the BlogApp.Posts context. So let's get back to the
lib/blog_app/posts.ex
and add
this function after
add_comment
:
lib/blog_app/posts.ex
def get_number_of_comments(post_id) do
post = Posts.get_post!(post_id) |> Repo.preload([:comments])
Enum.count(post.comments)
end
Now, let's update BlogApp.PostView module in
lib/blog_app_web/views/post_view.ex
like so:
lib/blog_app_web/views/post_view.ex
defmodule BlogWeb.PostView do
use BlogWeb, :view
alias BlogApp.Posts
def get_comments_count(post_id) do
Posts.get_number_of_comments(post_id)
end
end
Finally, in the
index.html.eex
posts file we
can add these lines to update our post.
<th>Comments</th>
in the table head, and
<td><%= get_comments_count(post.id)
%></td>
in the corresponding table body.
Here's how our blog looks now:
Hurray! We've got our basic blog up and running!
Make sure you check out the official Elixir and Phoenix documentation, wich covers more advanced topics.
If we missed something, let's start a conversation on Twitter.