In this session, we are going to continue with the walkthrough introduction to Phoenix. We assume that you have the application at the same point where we left it during the lecture.

Adding users

The next action that we are going to analyze and implement is the addition of a user. In a web application, this consists of two steps: 1) get a form to enter the information of a user, and 2) take the information, which should be submitted using an HTTP POST, and store it into the database.

The first step can be triggered by an HTTP GET on /users (you can double-check this using mix phx.routes, which would print out the routing table or REST API). Such an interaction will be forwarded by the router to the controller UserController and, more specifically, to the the function UserController.new/2. Open the file lib/tacsi_web/controllers/user_controller.ex and copy there the following snippet:

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

At this point in time, I would assume that you understand the code above: It initializes a changeset, using the default values (null in this case) and then it calls the function render/3 with the reference to conn, to the file new.html (which is the one we expect to return to the user’s browser, and which will include the HTML form), and the newly created changeset.

As mentioned before, the render method is passed over to the view component that handles the rendering of users, i.e. UserView in file lib/tacsi_web/views/user_view.ex, which in this moment uses the default behavior. That is why the module UserView is almost empty. In fact, we will keep that file as is for this session.

Now, let us copy the following snippet in the file lib/tacsi_web/templates/user/new.html.eex.

<%= form_for @changeset, Routes.user_path(@conn, :create), fn user -> %>
  <div class="form-group">
    <%= label user, :name, class: "control-label" %>
    <%= text_input user, :name, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= label user, :username, class: "control-label" %>
    <%= text_input user, :username, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= label user, :password, class: "control-label" %>
    <%= text_input user, :password, class: "form-control" %>
  </div>

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

As you can see, the code above looks very similar to an HTML file. However, it has a lot of embedded Elixir annotations. For instance, the document starts with the tag <%= form_for @changeset, Routes.user_path(@conn, :create), fn user -> %>. In fact, every place that you find a tag with the prefix <% is a place where we want to include some Elixir code. In the latter tag, we are using a call to the function Phoenix.HTML.Form.form_for/4 that generates the HTML code for a form, tailored with the values of @changeset, etc. In the example above, we are asking Phoenix to create a form for the changeset associated to a user (in the beginning, the changeset is empty but may also already include some changes, e.g. when the form has been found with invalid values and needs to be corrected). The second parameter, i.e. Routes.user_path(@conn, :create), is actually a call to the function Routes.user_path/2, which would return the URL to be for activating the action :create. You may not have noted that the first column in the table dumped by mix phx.routes includes the name user_path. In fact, that column and the function that we are referring here (i.e. user_path) are both the same. The third parameter, which is not used in our example and is associated with a default value of [], corresponds to some configuration options. The last parameter is the beginning of an anonymous function! Yes, the rest of the form is the body of this anonymous function. The only parameter that is received by the anonymous function is, in fact, the changeset. In that place, I decided to use the variable name user because that makes the code easier to read, I believe.

Inside the form, you will see a series of groups and, for each attribute of user, we will have two tags. The first tag, e.g. <%= label user, :name, class: "control-label" %>, is a call to Phoenix.HTML.Form.label/3, which generates HTML for a label on the form. Similarly, the tag <%= text_input user, :name, class: "form-control" %> will produce an HTML text input tag. Note that in both tags, there is a reference to user and :name. This is because we want Phoenix to store the values captured by the form in a format that can be conceptualized by the JSON string: {user:{"name": "...", "username": ...}}.

If you click the submit button, you will see that the page on the web browser changes. Unfortunately, in this moment we only get an error report. If you analyze the error report, you will see that the application was trying to reach the action UserController.create/2, which is the one that is supposed to handle the HTTP POST on /users. Copy the following snippet to the file lib/tacsi_web/controllers/user_controller.ex.

  def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)

    Repo.insert(changeset)
    redirect(conn, to: Routes.user_path(conn, :index))
  end

The code above does the following. It extracts the values for the user information, by using pattern matching. Note that the JSON string, which I referred in the previous, is automatically converted into an Elixir map. The first line in the function takes the values associated with the user information and validates them (this is the aim of calling User.changeset/2). If you remember, the function User.changeset/2 was already checking that the values of :name, :username and :password would not be null or empty string. For the moment being, we will assume that such situation does not happen. Once the changes are validated we store the information in the database by executing Repo.insert(changeset). Again, we are assuming that the database operation always succeeds. We are going to discuss later how to handle the errors.

The list line in the function redirects the application to :index. That means that the application will display the list of all users in the system.

In this moment, you can access the form to start the insertion of a new user by entering the URL http://localhost:4000/users/new on your browser. However, we can also add a link, say in the index page for users. To that end, you can copy the following line at the end of the file lib/tacsi_web/templates/user/index.html.eex.

<span><%= link "New user", to: Routes.user_path(@conn, :new) %></span>

Well, we would better change the entire template. Please replace the current content of lib/tacsi_web/templates/user/index.html.eex with the following snippet.

<h2>Listing users</h2>

<table class="table">
  <thead>
    <tr>
      <th>Name</th>
      <th>Username</th>
      <th>Password</th>

      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for user <- @users do %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.username %></td>
      <td><%= user.password %></td>

      <td class="text-right">
        <span><%= link "Show", to: Routes.user_path(@conn, :show, user), class: "btn btn-default btn-xs" %></span>
        <span><%= link "Edit", to: Routes.user_path(@conn, :edit, user), class: "btn btn-default btn-xs" %></span>
        <span><%= link "Delete", to: Routes.user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %></span>
      </td>
    </tr>
<% end %>
  </tbody>
</table>

<span><%= link "New user", to: Routes.user_path(@conn, :new) %></span>

Editing users

Let us now consider the operation of editing the information about a user. It turns out that this operation is quite similar to the previous one, in the sense that it uses a form to let one change one or several values. I think in lots of contexts it does not make sense to change the name or username, but we will allow that situation for now.

What is interesting here is that we can use the same HTML form for both actions. However, we need to refactor a bit the EEX template.

Start by changing the file name from lib/tacsi_web/templates/user/new.html.eex to lib/tacsi_web/templates/user/form.html.eex. Then, we are going to replace the first line of the file with the following one:

<%= form_for @changeset, @action, fn user -> %>

As you can see, the change involves replacing Routes.user_path(@conn, :create) by @action. Let me continue and I will explain the reason of that change a bit later. Now, create a new file lib/tacsi_web/templates/user/new.html.eex and copy there the following snippet:

<h2>New user</h2>

<%= render "form.html", changeset: @changeset,
                        action: Routes.user_path(@conn, :create) %>

<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>

Save the file and check if the application still works for adding users. What you can see is that in the HTML above, we are using a function render/2 to embed the template form.html in that place. The call passes on the changeset and, interestingly, sets action to user_path(@conn, :create) (yes, the value that we are going to access as @action in the template form.html). Using the same approach we can now add, on file lib/tacsi_web/templates/user/edit.html.eex, a template to handle the edition of user information:

<h2>Edit user</h2>

<%= render "form.html", changeset: @changeset,
                        action: Routes.user_path(@conn, :update, @user) %>

<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>

Different from the previous form, the value of action is set to user_path(@conn, :update, @user).

Let us now add the code to the controller. As for creation, editing the user information requires to HTTP interactions. In the first one, we retrieve the HTML form and, in the second one, we validate the changes and store the new version in the database.

Open the file lib/tacsi_web/controllers/user_controller.eex and add the following code:

  def edit(conn, %{"id" => id}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user, %{})
    render(conn, "edit.html", user: user, changeset: changeset)
  end

The code above corresponds to the first step, that is, getting a form to edit the information. Note that the code resembles a lot the one for creation. Except that we first have to retrieve the information stored in the database. To that end, we call Repo.get!(User, id), where the id is provided as part of the function input parameters. For now, we are assuming that the requested id exists in the database. We will later see how to handle the case where the id is not found in the database. With the information retrieved from the database, we initialize a changeset which is then passed on to the view component using the function render. Note that now the render function is called with a reference to the template edit.html, which we described at the beginning of this section.

Now, copy the following code into the file lib/tacsi_web/controllers/user_controler.ex.

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user, user_params)

    Repo.update(changeset)
    redirect(conn, to: Routes.user_path(conn, :index))
  end

The code above corresponds to the function that is called with an HTTP PUT/PATCH on /users/:id to update the information about a user. As you can see, we use pattern matching to extract the information about the id and the values of the attributes associated with the user. This time, we need to retrieve the information of the user as stored in the database with Repo.get!(User, id). Then, we validate the intended changes by calling User.changeset(user, user_params). If the changes apply then we can update the database with Repo.update(changeset) and we finish by redirecting to :index where we expect to see the list of users already updated.

Providing feedback

Up to now, we were overlooking the errors that could arise when we want to store a new or updated version of user in the database. If you remember, we are already using a very simple validation in the function changeset/2 defined in lib/tacsi_web/models/user.ex, namely asserting that the values of all the attributes of a user are required (i.e. |> validate_required([:name, :username, :password])). However, we are not warning the user whenever any of those values is missing.

Let me then present the approach step by step. Note that all the changes will happen in the template lib/tacsi_web/templates/user/form.html.eex and in the controller lib/tacsi_web/controllers/user_controller.ex.

First, we are going to add a warning text on the top of the form. That warning will be added with the following HTML block:

  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

It turns out that @changeset.action will be set to something different from null in case a validation error is found in the changeset. That means that the message in the block above will be displayed only on the occurrence of an error.

Then, on the group associated to each one of the attributes, we will add the corresponding error signaling tag, as exemplified below.

  <div class="form-group">
    <%= label user, :username, class: "control-label" %>
    <%= text_input user, :username, class: "form-control" %>
    <%= error_tag user, :username %>
  </div>

Note that error_tag will generate an HTML element that will render the corresponding message error, if any. In our example, the message error will be the absence of value for attribute :username. Please update the rest of the form to include error tags for the other two attributes.

Let me now illustrate the changes on the controller to handle the validation errors. The following snippet presents the new version of the function for creating a user.

  def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)

    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: Routes.user_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

You can see that we try to save the new user in the database, by passing the corresponding (validated) changeset. However, if the changeset has found a validation error, the operation Repo.insert(changeset) will fail. If you remember, in Elixir it is customary to use pattern matching and tuples {:ok, ...} or {:error, ...} to signal success or failure, respectively. Therefore, we use a case to match each one of the possibilities. In case of error, the controller instruct the view to rerender the template new.html (actually, the errors would have been detected in the same template), passing the changeset. Note that the changeset now includes the information about the errors.

In case of success, we do something else: we add a success message. The success message is then piggy backed to the HTTP response in what is called the “flash”. If you open the file lib/tacsi_web/templates/layout/app.html.eex you will find a block that looks as the one below.

      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

Clearly, such block serves to display any eventual success (flash :info) or error (flash :error) message. Now, let me clarify that the template lib/tacsi_web/templates/layout/app.html.eex is a sort of root template, which defines the layout of the other templates, for instance with global header and footer blocks. So, the place holder for flash will be visible in any of the pages generated for the application.

Showing users

Let us now have a look at the changes required to show the information of a user. Fortunately, in the first part of this session, we have added buttons to the right-hand side of each user to trigger several actions, and showing a user’s information is one of them.

Therefore, let us now analyze the changes on the code to implement the support for showing the user’s information. Among them, the following snippet corresponds with the code to be added to the controller.

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

Simple, we retrieve the information of a user, given his/her identifier, from the database. Then, we forward such information to the view component, indicating that the template to use is show.html. To complete the changes, copy the following code into the file lib/tacsi_web/templates/user/show.html.eex.

<h2>Show user</h2>

<ul>

  <li>
    <strong>Name:</strong>
    <%= @user.name %>
  </li>

  <li>
    <strong>Username:</strong>
    <%= @user.username %>
  </li>

  <li>
    <strong>Password:</strong>
    <%= @user.password %>
  </li>

</ul>

<span><%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %></span>
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>

I think the code above should already be clear for you, so let us move on.

Deleting users

To complete the walkthrough, we will analyze the code required to implement the delete operation on users. But first, let me bring a piece of code that we added at the end of the first part of this session. I copied the code below.

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

The code above adds a button to trigger the delete operation on a user. What is new here is that we can ask phoenix to add a confirmation dialog with the message Are you sure?. Indeed, the operation is used quite often such that it makes sense to include it as part of the library. Note that the button will produce an HTTP DELETE on /users/:id and, hence, the request will eventually reach the function delete on UserController. The code below is the one we have to copy into our project:

  def delete(conn, %{"id" => id}) do
    user = Repo.get!(User, id)
    Repo.delete!(user)

    conn
    |> put_flash(:info, "User deleted successfully.")
    |> redirect(to: Routes.user_path(conn, :index))
  end

Since the function Repo.delete/2 expects a changeset or struct, we need first to query the database for the user. Once we have the user, we can request its deletion. Note that both Repo.get!/3 and Repo.delete!/2 have bang (!) as its suffix. This is because we assume that the user can be found and then removed without any problem. If a problem exists, then the functions would raise an exception. The rest of the code has been already described elsewhere.

This concludes the walkthrough. Please take sometime to repeat the walkthrough several times to get to know the structure of a Phoenix-based application.