2 years ago
How to Build Elixir Blog App With Phoenix in Less Than 15min
Okay, let’s get real for a second. No-one can actually build a production-ready blog application from scratch in less than 15 minutes. That’s not how the world works. However, creating a working prototype — that’s totally possible.
So here is the plan — we are gonna spend the next 15min or so building a prototype of a blog application that meets the following criteria:
User can create, update, display and delete posts
User can add comments to a post
User can see all the comments for a particular post
User can see how many comments does each post have
A small side note — this blog post is inspired by an article I read a couple of days back — “Elixir Blog in 15 Minutes Using Phoenix Framework — Step By Step Tutorial” written by Jakub Cie?lar. Since the article was published way back in 2015, I felt it will be nice to get an updated version that properly reflects the current state of Elixir and Phoenix.
STEP 0: Prerequisites
The Phoenix Framework has a fantastic set of installation docs. I suggest you follow these and you’ll be set up and ready to go in no time.
Another small side note — if you are planning to explore and work with different versions of Elixir in the future, you might want to install Elixir (and Erlang) via asdf.
STEP 1: Create the blog application
First of all, meet your new best friend — Mix. This is an Elixir build tool that provides a basic set of tasks to help create and manage Elixir apps.
To create a new application from scratch, all you need to do is open a new terminal window and enter the following command:
mix phx.new blog
Halfway through the installation process you will be asked if you want to fetch and install the project dependancies — I suggest you go ahead and do so.
* creating blog/config/config.exs
* creating blog/config/dev.exs
* creating blog/config/prod.exs
...
* creating blog/assets/css/phoenix.css
* creating blog/assets/static/images/phoenix.png
* creating blog/assets/static/robots.txt
Fetch and install dependencies? [Yn]
If you choose not to install the dependancies, you must make sure to run the below mentioned mix tasks in order to complete your setup process. Otherwise, you won’t be able to start your application.
* running mix deps.get
* running mix deps.compile
* running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
And that’s it — you now have a working application. Well… not just yet. There are still a couple of extra things you need to take care of first.
Check the dev.ex and test.ex files that have been created under the config folder of your project. Make sure that the database username and password match the ones you have setup on your local machine.
config :blog, Blog.Repo,
username: "postgres",
password: "postgres",
database: "blog_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
You need to create the database of your application
$ mix ecto.create
The database for Blog.Repo has been created
And now it’s done. You can go ahead and start the server using the following command:
$ mix phx.server
By default your app will be running on http://localhost:4000
STEP 2: Setting up the Post resource
Phoenix provides us out of the box with a neat set of generators — mix tasks that are responsible for setting up all the modules we need to spin up a resource in a fast and efficient manner.
To create a new Post resource we are gonna use phx.gen.html — it will generate the controller, views and context for us.
mix phx.gen.html Posts Post posts title:string body:text
Basically, we just told mix that we want to create a new resource named Post that has context Posts and a database table posts that has two fields — title of type string and body of type text.
Next, we need to go to the Blog.Router module and add the route definitions for our new resource:
resources "/posts", PostController
Adding the new Post resource has generated a new database migration. To persist the changes in the database, we must run the following command:
$ mix ecto.migrate
Now we can start the server via mix phx.server and see the new changes by hitting the http://localhost:4000/posts route.
STEP 3: Adding Comments to a Post
To create a new Comment resource we are gonna use phx.gen.context — another phoenix generator that will help us to setup a context with some functions around an ecto schema.
mix phx.gen.context Comments Comment comments name:string content:text post_id:references:posts
A closer look at the command above tells us that we have used the generator to define the relationship between Post and Comment by adding the post_id field to the comments table.
However, we still need to manually define the association between the two schemas. To do that we will use the functions — belongs_to and has_many.
If you want to know more about them, you can check the Ecto.Schema docs.
A small side note — all of the code samples in this blog post are using aliases to substitute the full module names. That’s why writing belongs_to(:post, Post) is equal to belongs_to(:post, Blog.Posts.Post)
defmodule Blog.Comments.Comment do
use Ecto.Schema
import Ecto.Changeset
alias Blog.Posts.Post
schema "comments" do
field :content, :string
field :name, :string
belongs_to(:post, Post)
timestamps()
end
@doc false
def changeset(comment, attrs) do
comment
|> cast(attrs, [:name, :content, :post_id])
|> validate_required([:name, :content, :post_id])
end
end
defmodule Blog.Posts.Post do
use Ecto.Schema
import Ecto.Changeset
alias Blog.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
After making the above changes, don’t forget to run mix ecto.migrate ;)
Having properly set up our schema associations, we turn our attention to completing the task at hand — we want to enable a user to add a comment to a blog post from the UI.
First, we are gonna add a new route to our Blog.Router module:
resources "/posts", PostController do
post "/comment", PostController, :add_comment
end
Next, we are gonna add the add_comment action to our post_controller.ex
It will create a new comment in the database and associate it to a post.
In addition, it will load the post page and display the status of the create operation in a flash message.
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, "Comment added :)")
|> redirect(to: Routes.post_path(conn, :show, post))
{:error, _error} ->
conn
|> put_flash(:error, "Comment not added :(")
|> redirect(to: Routes.post_path(conn, :show, post))
end
end
Personally, I am not a fan of big controller modules. That’s why I have chosen to put the add_comment function as part of Blog.Posts context module.
def add_comment(post_id, comment_params) do
comment_params
|> Map.put("post_id", post_id)
|> Comments.create_comment()
end
Next, we are going to implement a simple Add Comment form and add it to the page that displays a single post. For that to happen, we need to modify the show action in the post controller. We are going to add the Comment.changeset and pass it to the show.html template.
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 we are going to create a new comment_form.html template that will be used to enter and save comment data.
<%= 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 %>
Lastly, we need to add the comments_form.html template to the show.html and make sure to pass it the correct data:
<%= render "comments_form.html", post: @post, changeset: @changeset, action: Routes.post_post_path(@conn, :add_comment, @post) %>
http://localhost:4000/posts/<post_id>
STEP 4: Displaying all comments for a given post
To display all the comments of a blog post we will create a new template — comments.html
<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>
We can pretty up the template by adding some css styles. These can be inserted into the main app.scss file.
.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;
}
Lastly, we need to insert the comments.html template into the show.html and make sure to pass it the correct data.
<%= render “comments.html”, comments: @post.comments %>
http://localhost:4000/post/<post_id>
STEP 5: Displaying number of comments per post
We can write a simple function to calculate the number of comments associated with a post and add it to the Blog.Posts context.
def get_number_of_comments(post_id) do
post = Posts.get_post!(post_id) |> Repo.preload([:comments])
Enum.count(post.comments)
end
Next we can call that function from the BlogWeb.PostView module in the following manner:
defmodule BlogWeb.PostView do
use BlogWeb, :view
alias Blog.Posts
def get_comments_count(post_id) do
Posts.get_number_of_comments(post_id)
end
end
Lastly we can extend the index.html template by adding a couple of lines of code that will help us to display the number of comments.
<th># of Comments</th>
...
<td><%= get_comments_count(post.id) %></td>
Total Comments: 1
News