Subscribe to access all episodes. View plans →

#88: Phoenix LiveView Part 2

Published April 2, 2019

Phoenix 1.4

Elixir 1.7

Phoenix LiveView

View source on GitHub


In episioode #87 we configured an existing application to use Phoenix LiveView and updated our album page to use it. Now let’s build some additional features leveraging LiveView.

Let’s update our albums so that users can edit them, right here form the index page. We’ll want it so that when a user clicks on an album’s summary, it becomes editable.

Let’s get started. We’ll open our album index.html.leex live view template and where we’re displaying each of our albums, let’s update it so that if there’s an @editable_id that matches an album’s ID we’ll render a form to edit an album’s summary.

I’ll go ahead and paste in our form for an album’s summary. Otherwise, let’s display our album’s summary. Now in order for an album to become editable, our users will need to click on it. Phoenix LiveView comes with event bindings, let’s use the phx-click event binding here and for the event name, let’s use “edit” followed by the album’s ID. This way we can pattern match to get the ID of the album we want to make editable. It also looks like we have a missing “do” in our if statement, let’s go ahead and add that.

Template path: lib/teacher_web/templates/album/index.html.leex

...
<%= for album <- @albums do %>
  <tr>
    <td><%= album.artist %></td>
    <td><%= album.title %></td>
    <td><%= album.category.name %></td>
    <%= if @editable_id == album.id do %>
    <td>
      <%= form_for Teacher.Recordings.change_album(album), "#", fn f -> %>
        <%= hidden_input f, :id, name: "id" %>
        <%= text_input f, :summary %>
        <%= submit "Update" %>
      <% end %>
    </td>
    <% else %>
      <td phx-click="edit<%= album.id %>" class="editable-field">
        <%= album.summary %>
      </td>
    <% end %>
    <td><%= album.year %></td>
    <td>
      <%= link "Show", to: Routes.album_path(@socket, :show, album) %>
    </td>
  </tr>
<% end %>
...

Now that we’ve updated our template, we’ll need to add the corresponding handle_event callback in our Live View module. Let’s open our AlbumLive.Index module and we’ll define a handle_event pattern matching on the “edit” string and our album ID. It also will some parameters, which we can ignore here since we wont use it and the socket.

Then inside the function, let’s convert our album_id from a string to an integer. We’ll update our socket assigns to include the editable_id of the album that was clicked. Let’s also update our mount so the initial editable_id is nil.

lib/teacher_web/live/album_live/index.html.ex

...
def mount(_session, socket) do
  albums = Recordings.list_albums()
  {:ok, assign(socket, albums: albums, editable_id: nil)}
end

...

def handle_event("edit" <> album_id, _, socket) do
  album_id = String.to_integer(album_id)
  {:noreply, assign(socket, editable_id: album_id)}
end

Now let’s go to the browser and test out our changes. Great - clicking on our album’s summary makes it editable. What’s happening here is when we click on an album’s summary, the phx-click event is fired, which then updates the socket assigns in the handle_event callback we just wrote. And when our socket’s assigns change our render function is automatically invoked, with the changes sent to the client, which results in our form now being displayed. Now that our form is being displayed, we need to update it so that when it’s submitted, the changes are saved to the database.

Back in our index.html.leex template let’s add another event binding - phx_submit - to our form, giving it the event name save. Let’s also include phx_disable_with for when our form is submitted. And we can give it some text that we want to display on our button when our form is submitted.

We’ll also make another change to our form. Currently we’re returning an empty changeset for our form. However, the way we’re defining it wont allow us to track changes and display errors to our users. Instead, we should create our changeset in our LiveView module and then set it in our socket’s assigns and then when our form is submitted, we can re-render our changeset to display any errors. Let’s update our form with a changeset and then we’ll include an error tag so any errors are displayed.

Template path: lib/teacher_web/templates/album/index.html.leex

...
<%= for album <- @albums do %>
  <tr>
    <td><%= album.artist %></td>
    <td><%= album.title %></td>
    <td><%= album.category.name %></td>
    <%= if @editable_id == album.id do %>
    <td>
      <%= form_for @changeset, "#", [phx_submit: :save], fn f -> %>
        <%= hidden_input f, :id, name: "id" %>
        <%= text_input f, :summary %>
        <%= error_tag f, :summary %>
        <%= submit "Update", phx_disable_with: "Saving..." %>
      <% end %>
    </td>
    <% else %>
      <td phx-click="edit<%= album.id %>" class="editable-field">
        <%= album.summary %>
      </td>
    <% end %>
    <td><%= album.year %></td>
    <td>
      <%= link "Show", to: Routes.album_path(@socket, :show, album) %>
    </td>
  </tr>
<% end %>
...

Then we’ll go to our AlbumLive.Index LiveView module and let’s create the changeset in our handle_event “edit” callback that’s called when an album summary is clicked. We’ll find the selected album from the socket’s assigns. Then we’ll pipe it into Recordings.change_album to get the changeset and set the :action as :update. Then let’s include it as part of the socket’s assigns.

Now let’s add our handle_event callback for our “save” event. We’ll pattern match on the album ID and the album params from the form data that’s submitted. Then we’ll update that album with the new summary using the Recordings.update_album function and we’ll again convert our album_id from a string to an integer. Let’s find the album we want to update in our socket assigns. Then we’ll call Recordings.update_album with our album and the new summary contained in the album_params. We’ll pattern match on the OK tuple and the error tuple. If our album is updated we’ll return a :stop tuple and add a flash message to let the user know it was updated and then redirect to our album’s index path.

If there’s an error updating our album we’ll return a :noreply tuple with the socket, assigning the updated changeset to the socket so our errors are displayed and since we’re including our Routes helper, we’ll need to add an alias for it.

lib/teacher_web/live/album_live/index.ex

...
alias TeacherWeb.Router.Helpers, as: Routes
...
def handle_event("edit" <> album_id, _, socket) do
  album_id = String.to_integer(album_id)
  changeset = socket.assigns.albums
    |> Enum.find(&(&1.id == album_id))
    |> Recordings.change_album()
    |> Map.put(:action, :update)
  {:noreply, assign(socket, changeset: changeset, editable_id: album_id)}
end

def handle_event("save", %{"id" => album_id, "album" => album_params}, socket) do
  album_id = String.to_integer(album_id)
  album = Enum.find(socket.assigns.albums, &(&1.id == album_id))
  case Recordings.update_album(album, album_params) do
    {:ok, _album} ->
      {:stop,
        socket
        |> put_flash(:info, "Album updated successfully")
        |> redirect(to: Routes.album_path(socket, :index))}
    {:error, changeset} ->
      {:noreply, assign(socket, changeset: changeset)}
  end
end
...

Then let’s go back to the browser and then we’ll select an album’s summary that we want to edit. Let’s go ahead and update the summary and then when we submit our form. Great - our album is updated. Great - our album was updated and our flash message was displayed.

Now let’s try submitting an empty summary and we get an error - album summary can’t be blank. Then if we try a single character we see another error - our album summary should be at least 3 characters.

We’re now able to update album data and display validations using Phoenix LiveView.

© 2024 HEXMONSTER LLC