Subscribe to access all episodes. View plans →
Published January 23, 2017
Elixir 1.3.4
Phoenix 1.2.1
Episode source code on GitHub
Let’s pick up where we left off in Getting Started with Phoenix.
Now that we can create posts on our blog, let’s take the next step and add comments.
We’ll use another Phoenix task, this time to generate the model. We’ll use a singular module name ‘Comment’, and the plural version ‘comments’ for the database table.
Then a list of attributes for our comments. In this case we’ll want them to have a body that’s a text. And since comments will belong to a post, we’ll create a ‘post_id’ and reference posts. This will help setup our association.
$ mix phoenix.gen.model Comment comments body:text post_id:references:posts
Now before we migrate our database let’s look at the comment model it generated.
Since we referenced ‘posts’, the belongs_to :post
association was setup for us. Now let’s setup the other half of this relationship.
We’ll open our post module and add the has_many :comments
association to the schema.
web/models/post.ex
defmodule Teacher.Post do
use Teacher.Web, :model
schema "posts" do
field :title, :string
field :body, :string
has_many :comments, Teacher.Comment
end
...
end
Since comments and posts have an association what would be the best way to delete all comments associated with a post if that post is deleted?
To do this let’s go to the database migration that was just created for comments: priv/repo/migrations/{{timestamp}}_create_comment.exs
When we add the ‘post_id’ column, we’re given the :on_delete
option. By default it’s set to :nothing
. If we change it to :delete_all
this will trigger all of a post’s comments to be deleted along with it. With that changed, we can run our database migration.
$ mix ecto.migrate
Now we need to add a form to our blog so people can add comments. Let’s start by adding a new controller named comment_controller.ex
. And in it we’ll define a create function. It will take the connection and pattern match to get the comment params and the post id.
web/controllers/comment_controller.ex
defmodule Teacher.CommentController do
use Teacher.Web, :controller
alias Teacher.Post
def create(conn, %{"comment" => comment_params, "post_id" => post_id}) do
post = Repo.get(Post, post_id)
comment_changeset = Ecto.build_assoc(post, :comments, body: comment_params["body"])
Repo.insert(comment_changeset)
conn
|> put_flash(:info, "Comment created successfully.")
|> redirect(to: post_path(conn, :show, post))
end
end
Let’s walk through what this function’s doing.
Inside we lookup the post
with the post_id
.
Then build a comment changeset. Ecto.build_assoc/3
builds an association between comment and post and sets the value of the body key from our comment params as our comment’s body attribute.
Finally we’ll insert it into the database and then redirect to our post.
With our controller good-to-go, let’s build the view for our comments, adding a CommentView
module.
web/views/comment_view.ex
defmodule Teacher.CommentView do
use Teacher.Web, :view
end
Now we can add our template. First we need to create a comment directory. And then a file in that directory named new.html.eex
Then we can add our form:
web/templates/comment/new.html.eex
<%= form_for @comment_changeset, post_comment_path(@conn, :create, @post), fn f -> %>
<%= label f, :body, class: "control-label" %>
<%= text_input f, :body, class: "form-control" %>
<%= error_tag f, :body %>
<%= submit "Submit", class: "btn btn-primary" %>
<% end %>
We’ll use the Phoenix form_for
helper passing in our comment changeset and our action, which we’ll define in our routes later.
Now that we have our form, let’s display it beneath our posts.
Let’s open our post’s show template. We’ll render our new template here, using the render/3
function. It takes the module, in this case Teacher.CommentView
, the template, and a set of assigns, which will be our connection, post, and comment changeset. All three are used in our template.
web/templates/post/show.html.eex
...
<%= render Teacher.CommentView, "new.html",
conn: @conn,
post: @post,
comment_changeset: @comment_changeset %>
...
Now we need to add the route that our comment form will post to.
So let’s open up our router. And we’ll add our comments resource just below our posts.
web/router.ex
defmodule Teacher.Router do
...
resources "/posts", PostController
resources "/comments", CommentController
...
end
Since comments have an association with posts, let’s nest comments in posts. This will include the post’s id in the params, making it easier for us to associate the comment with its post. And for the purposes of this demo we don’t need all of the routes that resources macro builds. So let’s limit this to only have a route to create comments.
web/router.ex
defmodule Teacher.Router do
...
resources "/posts", PostController do
resources "/comments", CommentController, only: [:create]
end
...
end
Now let’s go to a post in our browser. And we’ll see an error.
This is because we are calling the comment changeset in our comment form, but haven’t defined it. Let’s fix that.
We’ll go to our PostController
and update the show
function. Creating the comment_changeset
that our form needs. Then we’ll pass in the comment_changeset
so it can be accessed by our template.
There’s one quick improvement we can make to this. Having to write Teacher.Comment
every time we want to use comment is a little long, but Elixir provides a simple way for us to shorten it and just write Comment
.
Going to to top of our controller, let’s update our alias to include comment in addition to post. Now we can go back and remove Teacher
from Teacher.Comment
.
web/controllers/post_controller.ex
defmodule Teacher.PostController do
use Teacher.Web, :controller
alias Teacher.{Post, Comment}
...
def show(conn, %{"id" => id}) do
post = Repo.get!(Post, id)
comment_changeset = Comment.changeset(%Comment{})
render(conn, "show.html", post: post, comment_changeset: comment_changeset)
end
...
end
Now if we go back to our post - we see our form.
Let’s test it out and add some comments.
And if we check our database directly, we can see that our comments were saved.
Now we just need to display it below our post.
Going back to our show
action on our PostController
we’ll need to explicitly tell Ecto that we want to load all the comments for our post.
web/controllers/post_controller.ex
def show(conn, %{"id" => id}) do
post = Repo.get(Post, id)
post = Repo.preload(post, :comments)
comment_changeset = Comment.changeset(%Comment{})
render(conn, "show.html", post: post, comment_changeset: comment_changeset)
end
This will work, but let’s update it to take advantage of one of Elixir’s great features the pipe operator.
The pipe operator will take whatever is returned and pass it in as the first argument of the subsequent function call on it’s right side.
So rewriting this - Post
gets passed in initially as the first argument in Repo.get/3
, which then returns a post struct.
That post struct is then passed in as the first argument of Repo.preload/3
.
web/controllers/post_controller.ex
def show(conn, %{"id" => id}) do
post = Post
|> Repo.get(id)
|> Repo.preload(:comments)
comment_changeset = Teacher.Comment.changeset(%Teacher.Comment{})
render(conn, "show.html", post: post, comment_changeset: comment_changeset)
end
Now that we’re loading our comments, let’s update our Post’s show template.
web/templates/post/show.html.eex
...
<%= for comment <- @post.comments do %>
<%= comment.body %>
<% end %>
...
Here we’re just looping through each of our post’s comments and displaying it’s body.
And if we come back to our post - we can see it’s comments.
Thomas Brewer
6 years agoWhat GUI are you using with PostgreSQL?
Alekx
6 years agoHey Thomas, I’m using Postico.
Ravindra Kumar Yadav
5 years agoGetting this error:
Compiling 16 files (.ex)
== Compilation error in file web/models/post.ex == ** (UndefinedFunctionError) function Teacher.comment/0 is undefined or private
Alekx
5 years agoIs this for the
has_many
relationship? It looks like you’re trying to call the functioncomment
on theTeacher
module, which doesn’t exist. Make sure it’sTeacher.Comment
.Also a more current episode: https://elixircasts.io/phoenix-contexts uses Phoenix 1.4 and covers many of the same topics.
David Langford
4 years agoThanks for the great videos. With the following:
It’s not clear to me why you do that, instead of allowing
post_id
be cast in thechangeset/2
like belowI could not work out how to edit the comment in my app following your method, so I did the latter. Am I going to run into headaches later?
Alekx
4 years agoI’ve been planning on remaking this video, because as you see, there are some limitations with how it’s implemented.
Your solution is what I would go with to handle creating and then updating comments 👍