Last time we have covered the models and we have created a rooms model to keep information about them. Even though those rooms are in the database now, we are not able to manage them. Let’s do that by implementing all available CRUD operations.

What is CRUD?

CRUD is basically an acronym for the Create, Read, Update, and Delete. Which are basic functions of persistence storage, but also mostly used in building web interfaces.

We are going to implement them one by one.

On the left side of our page, we already have the list of the rooms.

Let’s implement functionality to create them.

C for Create

How would we create the room in our app?

As a first step, a user will probably click the “New Room” button. Then sees the form with fields he needs to fill in. He fills that form in and clicks “Submit” button. Using submitted data we are going to create the record in the database and redirect the user back to the list.

So we need a button. Let’s render it next to our “Rooms” title.

Replace the following line in the lib/prater_web/templates/room/index.html.eex file:

<h3>Rooms</h3>

with

<h3>
  Rooms <%= link "+", to: "/rooms/new", class: "btn btn-success" %>
</h3>

Here we have used Phoenix HTML link helper to render a link on the page. That is equivalent of the following code:

<h3>Rooms <a href="/rooms/new" class: "btn btn-success">+</a></h3>

So now we have a link next to our “Rooms” title. We click it, we get an error message:

no route found for GET /rooms/new (PraterWeb.Router)

The Phoenix does not aware of that URL and does not know which action should be used.

To fix that we need to update Router in the lib/prater_web/router.ex file. We already have a route for RoomController. Let’s add new route right next to it.

get "/", RoomController, :index # <- This line already exists
get "/rooms/new", RoomController, :new

Then we follow the error messages and fix them. To fix:

function PraterWeb.RoomController.new/2 is undefined or private

We create new action in our RoomController:

def new(conn, _params) do
  render(conn, "new.html")
end

As we already know from the Controllers and Views article, an action has to either render the view or redirect user somewhere. We need to render the form with inputs for the user, so we are rendering a new page. We also need to create a lib/prater_web/templates/room/new.html.eex template in order to avoid the following error:

Could not render "new.html" for PraterWeb.RoomView

We are going to create it with the simple title.

<h2>Add new room</h2>

We do it to check we are on the right page. And we are

Now let’s render the form by using the following markup.

<h2>Add new room</h2>

<%= form_for @changeset, room_path(@conn, :create), fn f -> %>
  <div class="form-group">
    <%= label f, :name, class: "control-label" %>
    <%= text_input f, :name, class: "form-control" %>
    <%= error_tag f, :name %>
  </div>

  <div class="form-group">
    <%= label f, :description, class: "control-label" %>
    <%= textarea f, :description, class: "form-control", rows: 5 %>
    <%= error_tag f, :description %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

Now we see the following error on the page:

assign @changeset not available in eex template.

That is correct. We didn’t pass the @changeset down to the template yet.

def new(conn, _params) do
  alias Prater.Conversation.Room
  changeset = Room.changeset(%Room{}, %{})
  render conn, "new.html", changeset: changeset
end

We set it to an empty Room and pass it to render function. Now the error message has been changed.

No function clause for PraterWeb.Router.Helpers.room_path/2 and action :create.

We are using room_path(@conn, :create) following function as an target action for our form. But we didn’t define it yet. To do that we need to create a new route.

Phoenix uses RESTful way of naming urls. We need create action. The RESTful way expects create action to have POST HTTP method.

post "/rooms", RoomController, :create

Now it works, we can see the form on the page

As a next step, we need to submit our form. We didn’t implement functionality for that. So we are getting the following error:

function PraterWeb.RoomController.create/2 is undefined or private

We need to define our create action. Let’s paste the complete implementation and figure out what does it do in small steps:

def create(conn, %{"room" => room_params}) do
  alias Prater.Conversation.Room
  alias Prater.Repo
  %Room{}
  |> Room.changeset(room_params)
  |> Repo.insert()
  |> case do
    {:ok, room} ->
      conn
      |> put_flash(:info, "Room created successfully.")
      |> redirect(to: room_path(conn, :index))

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

We have a couple of aliases to reduce typing. Then we initialize the room and apply our Room.changeset using parameters from the form. Then we try to save it into the database. If it succeeded and Repo.insert returns us {:ok, room} then we are going to redirect the user back to the home page and display a message to him. If the insert was failed we will render the form where the user will be able to see validation errors.

If we try to create a new room using our form, it should work now.

The functionality works. Before we move to the next functionality of CRUD, let’s stop for a while and do some improvements.

Small refactoring

Let’s hide some implementation details into Conversation context. We are going to extract it out of new action.

def new(conn, _params) do
  changeset = Conversation.change_room(%Room{})
  render conn, "new.html", changeset: changeset
end

We are also moved the following aliases to the top of the controller module.

alias Prater.Conversation
alias Prater.Conversation.Room

The function Conversation.change_room/1 does not exist yet, so we need to create it. Open lib/prater/conversation/conversation.ex file and add following implementation.

def change_room(%Room{} = room) do
  Room.changeset(room, %{})
end

Now let’s move to create function. Let’s extract the creation logic into Conversation.create_room/1 function. Our create function should look like:

def create(conn, %{"room" => room_params}) do
  case Conversation.create_room(room_params) do
    {:ok, room} ->
      conn
      |> put_flash(:info, "Room created successfully.")
      |> redirect(to: room_path(conn, :index))

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

The same procedure as we did with new action, we need to create Conversation.create_room/1 function.

def create_room(attrs \\ %{}) do
  %Room{}
  |> Room.changeset(attrs)
  |> Repo.insert()
end

Why did we do all these steps? Now our controller looks more compact and some implementation details are hidden in the Conversation context.

Now you can go and create one more room to check if our code still working. It should work. Now we are moving to the next step.

R for Read

The next part of the CRUD is Read. We are able to see a list of the rooms. So this piece partially covered. The missing part is to show the page of a particular room.

The show action stands for that.

We start by adding the link.

Open the lib/prater_web/templates/room/index.html.eex file and change following line:

<li class="list-group-item"><%= room.name %></li>

to

<li class="list-group-item">
  <%= link room.name, to: room_path(@conn, :show, room.id) %>
</li>

Now we are going to have the link to the room page instead of text.

We are still missing the route. Let’s create it.

get "/rooms/:id", RoomController, :show

By specifying :id in the URL, we are saying that our URL will have id parameter, which can use to capture the room’s ID.

We have a route now, we need an action:

def show(conn, _params) do
  render conn, "show.html"
end

That is not the first action we are creating. We already have some experience doing that. We know we need a template file lib/prater_web/templates/room/show.html.eex. Let’s use a simple title to check if what we did before is working.

<h2>Room details</h2>

It works. Cool. But we cant see the room details, let’s fix that. We already passing the room ID into the URL. Let’s grab it and fetch the room information. Once we do that we would need to pass it down to the template.

def show(conn, %{"id" => id}) do
  room = Prater.Repo.get!(Room, id)
  render(conn, "show.html", room: room)
end

Now we have the room’s data and can display it on the page. Let’s update our template.

<h2><%= @room.name %></h2>

<div><%= @room.description %></div>

And we can see room’s details

Small refactoring

As usual, once we have done the implementation we can slow down for a while and refactor our code. Let’s hide implementation details in the Conversation module, by extracting:

Prater.Repo.get!(Room, id)

into new get_room!/1 function

def get_room!(id), do: Repo.get!(Room, id)

so the show action would look like:

def show(conn, %{"id" => id}) do
  room = Conversation.get_room!(id)
  render(conn, "show.html", room: room)
end

That’s it for “Read”.

U for Update

We are going to implement the update functionality. Similar to “create” we need a link, so a user will be able to navigate to the page with the edit form.

<div>
  <%= link "Edit", to: room_path(@conn, :edit, @room.id), class: "btn btn-default" %>
</div>

We need a route.

get "/rooms/:id/edit", RoomController, :edit

The same as with show URL, we need a room’s ID to know which room we are going to update. Now let’s create the edit action which will be responsible to render the form.

def edit(conn, %{"id" => id}) do
  room = Conversation.get_room!(id)
  changeset = Conversation.change_room(room)
  render(conn, "edit.html", room: room, changeset: changeset)
end

With some small difference comparing to show, we need to pass changeset to the template in order to render the form. Speaking of which. There it is:

<h2>Edit room: <%= @room.name %></h2>

<%= form_for @changeset, room_path(@conn, :update, @room), fn f -> %>
  <div class="form-group">
    <%= label f, :name, class: "control-label" %>
    <%= text_input f, :name, class: "form-control" %>
    <%= error_tag f, :name %>
  </div>

  <div class="form-group">
    <%= label f, :description, class: "control-label" %>
    <%= textarea f, :description, class: "form-control", rows: 5 %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

And we also need the last route to make it work.

put "/rooms/:id", RoomController, :update

Unlike create route, we need PUT HTTP method for updates.

The edit page has been loaded with the form on it and the “name” and “description” fields contain the room’s data as well. Try to change something and submit the form. You will see following error:

function PraterWeb.RoomController.update/2 is undefined or private

Of course. Let’s create the function.

def update(conn, %{"id" => id, "room" => room_params}) do
  room = Conversation.get_room!(id)

  room
  |> Room.changeset(room_params)
  |> Prater.Repo.update()
  |> case do
    {:ok, room} ->
      conn
      |> put_flash(:info, "Room updated successfully.")
      |> redirect(to: room_path(conn, :show, room))
    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "edit.html", room: room, changeset: changeset)
  end
end

First, we found the room we need to update. Then we are trying to apply Room.changeset with the parameters passed from the form. If we succeed with an update and get {:ok, room} back, we are redirecting a user back to room’s page and display a “Room update successfully” message. Otherwise, we render the form with validation messages on it.

Check it. It should work.

Small refactoring

Refactoring is becoming a new tradition and it should be. After a bunch of changes, we need to look around and improve some parts of the code.

As usually let’s hide the implementation details in Conversation context and replace following lines in update action:

room
|> Room.changeset(room_params)
|> Prater.Repo.update()
|> case do

with

case Conversation.update_room(room, room_params) do

and of course, implement that function

def update_room(%Room{} = room, attrs) do
  room
  |> Room.changeset(attrs)
  |> Repo.update()
end

You have probably notice that we have implemented two forms for the create and edit purposes. But the forms barely have a difference. The only difference is the URL we are using to submit the form. We can also eliminate code duplication.

Let’s extract the form in the lib/prater_web/templates/room/form.html.eex file with the following content.

<%= form_for @changeset, @action, fn f -> %>
  <div class="form-group">
    <%= label f, :name, class: "control-label" %>
    <%= text_input f, :name, class: "form-control" %>
    <%= error_tag f, :name %>
  </div>

  <div class="form-group">
    <%= label f, :description, class: "control-label" %>
    <%= textarea f, :description, class: "form-control", rows: 5 %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

That is the same form we have in our templates with the small difference. It has an @action variable instead of target URL.

Now we can update a new.html.eex template to use that form:

<h2>Add new room</h2>

<%= render "form.html", Map.put(assigns, :action, room_path(@conn, :create)) %>

We are injecting another template inside existing one and extend assigns map with the new action key which holds the target URL for the form.

We are also updating the edit.html.eex template with similar render call:

<%= render "form.html", Map.put(assigns, :action, room_path(@conn, :update, @room)) %>

The only URL is differs comparing to new.html.eex.

We are done. Check everything again and be sure it is still working. Let’s move on.

D for Delete

To implement the delete functionality we will start with the link again.

<%= link "Delete", to: room_path(@conn, :delete, @room), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger" %>

This link a little bit longer than usual. Now we have method: :delete parameter. That is because the link helper uses GET HTTP method by default. In case of delete, we need the method to be DELETE. We also want to prevent accidental removal of any room. So we ask the user to confirm his choice by passing data: [confirm: "Are you sure?"].

We need a route for that:

delete "/rooms/:id", RoomController, :delete

And the route has DELETE HTTP method.

The first implementation of the delete action looks like:

def delete(conn, %{"id" => id}) do
  room = Conversation.get_room!(id)
  {:ok, _room} = Prater.Repo.delete(room)

  conn
  |> put_flash(:info, "Room deleted successfully.")
  |> redirect(to: room_path(conn, :index))
end

We are fetching the room by its ID. And we call Repo.delete to delete it from the database. Then we redirect a user back to the rooms list.

Check it. Is it working?

That’s it. This time we don’t need any views. Because all we do we are removing the room and redirecting a user.

Small refactoring

Let’s extract the delete call.

{:ok, _room} = Prater.Repo.delete(room)

into

{:ok, _room} = Conversation.delete_room(room)

with the following implementation

def delete_room(%Room{} = room) do
  Repo.delete(room)
end

Done.

One more thing.

If we look into our Router. We can see a bunch of routes related to rooms. They all have similar goals. Is it possible to refactor? Yes, it is.

Meet the resources.

As soon as we follow Phoenix convention to define our routes. All of the following routes

get "/rooms/new", RoomController, :new
post "/rooms", RoomController, :create
get "/rooms/:id", RoomController, :show
get "/rooms/:id/edit", RoomController, :edit
put "/rooms/:id", RoomController, :update
delete "/rooms/:id", RoomController, :delete

can be replaced with a one-liner

resources "/rooms", RoomController

The resources function will create all required routes for us.

We can check it by running phx.routes task:

→ mix phx.routes
room_path  GET     /                PraterWeb.RoomController :index
room_path  GET     /rooms           PraterWeb.RoomController :index
room_path  GET     /rooms/:id/edit  PraterWeb.RoomController :edit
room_path  GET     /rooms/new       PraterWeb.RoomController :new
room_path  GET     /rooms/:id       PraterWeb.RoomController :show
room_path  POST    /rooms           PraterWeb.RoomController :create
room_path  PATCH   /rooms/:id       PraterWeb.RoomController :update
           PUT     /rooms/:id       PraterWeb.RoomController :update
room_path  DELETE  /rooms/:id       PraterWeb.RoomController :delete

As you can see we are not missing any route. All of them are here.

So that concludes the “D”.

Wrapping up

To wrap it up. Today we have learned how to create manageable resources using CRUD actions. We have covered all the standard CRUD functions. Now out users can create a room, can see the room’s details, they can also update the room and even delete it.

You can find all the changes we have done here on the GitHub page of the project.

You may say, that it is possible to create all these CRUD actions using the mix phx.gen.html generator. And it would be much faster.

You are right. But!

It is always worth to know how every piece works and be able to implement it manually. Also, you don’t need all CRUD actions for every resource all the time. Sometimes, or even, most of the time, you would need only several of them.

In the end, it is up to you which approach to use. Because now you know every step.

Take care. See you next time.