Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

PicChat: Messages

deprecated_liveview_and_ecto.livemd

PicChat: Messages

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Home Report An Issue Math GamePicChat: Image Upload

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • How can we navigate the user from the client, and from the server?
  • How do we test a LiveView?
  • How do we use Phoenix Components to abstract and reuse code?
  • How can a LiveView render different UI based on URL params?

Overview

This is a companion reading for the PicChat: Messages exercise. This lesson is an overview of how to work with LiveView and Ecto in a Phoenix application. It also covers LiveComponents and some of the other topics you’ll encounter when using the LiveView Generators.

See the example_projects/pic_chat project folder to contextualize the examples found throughout this lesson.

LiveGenerator

We can use mix phx.gen.live similar to how we have already used mix phx.gen.html to generate the boilerplate code for a resource.

mix phx.gen.live Context Schema table field_name:field_type

Live Routes

LiveComponents

Phoenix LiveComponent encapsulates the behavior of a LiveView (state, message handling, displaying html) into a sub-component that can be placed inside of another LiveView.

This allows us to break apart our LiveViews into several smaller components, that can even be re-used.

defmodule HelloComponent do
  # If you generated an app with mix phx.new --live,
  # the line below would be: use MyAppWeb, :live_component
  use AppWeb, :live_component

  def render(assigns) do
    ~H"""
    Hello, @name!
    """
  end
end

We use the Phoenix.Component.live_component/1 to render a LiveComponent. The id and module attributes are required. Other attributes are bound to the socket assigns.

<.live_component module={HelloComponent} id="hello" name={@name} />

Like LiveViews, LiveComponents can send messages using phx-* bindings. By default, the message will be sent to their parent LiveView.

flowchart
LiveComponent --event--> ParentLiveView 

Alternatively, you can use @myself to send a message to itself.

flowchart
LiveComponent --event--> LiveComponent 

  Say hello!

LiveComponents can define event handlers using handle_event/3.

def handle_event("greeting", _params, socket) do
  # ...
  {:noreply, socket}
end

See HexDocs: Phoenix.LiveComponent for more.

Follow Along: PicChat Messages

Over the next few lessons we’re going to build a Chat Application that can also upload images called PicChat. Our application will include a global chat where users can send messages and see the feed of messages update in real-time.

We’ll also save messages in a database with Ecto to demonstrate how to use CRUD (Create, Read, Update, Delete) actions with LiveViews.

You can see the completed PicChat application for the sake of reference.

In this lesson, we’re going to create the initial feed of messages without images. Follow along with each step of the instruction to build your own PicChat application.

Create The Phoenix Project

Create the Phoenix project

$ mix phx.new pic_chat

Make sure to create the Ecto database.

$ mix ecto.create

Create The Chat Context.

We’re going to create a Chat context with a Message resource in a messages table. Messages will have a :content and :from field.

We can use the mix phx.gen.live phoenix generator to create the scaffolding for CRUD actions using LiveViews.

$ mix phx.gen.live Chat Message messages content:text from:string 

The majority of this lesson will be reading through the generated code in order to better understand how we can build applications with LiveView.

Define Routes

Add our routes in router.ex. We’ll remove the PageController route so we can use the base url "/".

scope "/", PicChatWeb do
  pipe_through :browser

  live "/", MessageLive.Index, :index
  
  live "/new", MessageLive.Index, :new
  live "/:id/edit", MessageLive.Index, :edit

  live "/:id", MessageLive.Show, :show
  live "/:id/show/edit", MessageLive.Show, :edit
end

Notice that unlike with MVC generators, the generator only generates two LiveViews MessageLive.Index and MessageLive.Show. It then uses the :index, :new, :edit, and :show parameters to conditionally render a modal. We’ll dive into how this works in a moment.

Since we removed the PageController route, make sure to remove page_view_test.exs, page_view.ex, page_controller_test.ex, and page_controller.ex, and /page/index.html.heex.

The MessageLive.Index LiveView

Start your server.

$ mix phx.server

Visit http://localhost:4000 to view your MessageLive.Index page.

handle_params/3

When the MessageLive.Index module in live/message_live/index.ex mounts, it automatically invokes the handle_params/3 callback.

handle_params/3 accepts params from the url and the live_action atom (defined previously in our router.ex for the corresponding url). It then calls the apply_action/3 function to assign some values in our socket state.

The :message field on the socket is used when editing or showing a message. The :page_title field displays a title in the tab in your browser.

Here’s a documented version of these functions for the sake of understanding.

@impl true
def mount(_params, _session, socket) do
  # This assigns the result of `list_messages()` to the `:messages` key of `socket`.
  {:ok, assign(socket, :messages, list_messages())}
end

@impl true
def handle_params(params, _url, socket) do
  # The apply_action/3 function assigns values to the socket for the :edit, :show, and :index actions.
  {:noreply, apply_action(socket, socket.assigns.live_action, params)}
end

# Assigns Values To The Socket For The `:edit` Action.
defp apply_action(socket, :edit, %{"id" => id}) do
  socket
  |> assign(:page_title, "Edit Message")
  |> assign(:message, Chat.get_message!(id))
end

# Assigns Values To The Socket For The `:new` Action.
defp apply_action(socket, :new, _params) do
  socket
  |> assign(:page_title, "New Message")
  |> assign(:message, %Message{})
end

# Assigns Values To The Socket For The `:index` Action.
defp apply_action(socket, :index, _params) do
  socket
  |> assign(:page_title, "Listing Messages")
  |> assign(:message, nil)
end

Template

Our MessageLive.Index function doesn’t define a render/1 function. Instead it uses a template file. If you don’t define a render/1 callback function in a LiveView, it automatically attempts to render a template matching the module’s name.

In this case, it renders a index.html.heex template file. This file renders a list of messages using the :messages field from the socket.

<%= for message <- @messages do %>
  <tr>
    <td><%= message.content %></td>
    <td><%= message.from %></td>

    <td>
      <span><%= live_redirect "Show", to: Routes.message_show_path(@socket, :show, message) %></span>
      <span><%= live_patch "Edit", to: Routes.message_index_path(@socket, :edit, message) %></span>
      <span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: message.id, data: [confirm: "Are you sure?"] %></span>
    </td>
  </tr>
<% end %>

Creating A Message

The New button uses live_patch/2 to re-render the current LiveView with a different URL http://localhost:4000/new.

<span><%= live_patch "New Message", to: Routes.message_index_path(@socket, :new) %></span>

You can visit http://localhost:4000/new. or click on the New button to see a modal for creating messages.

The :new live_action

We’re going to walk through the somewhat complex flow how this form is rendered. Along the way we’re going to be introduced to many new concepts.

This re-triggered the handle_params/3 function in MessageLive.Index. handle_params/3 calls apply_action/3 for the :new action.

This assigns a new :page_title and :message field on our socket.

defp apply_action(socket, :new, _params) do
  socket
  |> assign(:page_title, "New Message")
  |> assign(:message, %Message{})
end

<.modal>

The index.html.heex template uses these fields to render a modal.

<%= if @live_action in [:new, :edit] do %>
  <.modal return_to={Routes.message_index_path(@socket, :index)}>
    <.live_component
      module={PicChatWeb.MessageLive.FormComponent}
      id={@message.id || :new}
      title={@page_title}
      action={@live_action}
      message={@message}
      return_to={Routes.message_index_path(@socket, :index)}
    />
  
<% end %>

@live_action comes from router.ex for our "/new" URL when we define the :new atom.

live "/new", MessageLive.Index, :new

The modal component is built into Phoenix in our PicChat.LiveHelpers module in live_helpers.ex.

def modal(assigns) do
  assigns = assign_new(assigns, :return_to, fn -> nil end)

  ~H"""
  
    
      <%= if @return_to do %>
        <%= live_patch "✖",
          to: @return_to,
          id: "close",
          class: "phx-modal-close",
          phx_click: hide_modal()
        %>
      <% else %>
        
      <% end %>

      <%= render_slot(@inner_block) %>
    
  
  """
end

``

render_slot/2 renders the HTML we place inside of the <.modal> element.

This renders the <.live_component> element inside of the modal in index.html.heex with some attributes.

<.live_component
  module={PicChatWeb.MessageLive.FormComponent}
  id={@message.id || :new}
  title={@page_title}
  action={@live_action}
  message={@message}
  return_to={Routes.message_index_path(@socket, :index)}
/>

LiveComponent: FormComponent

The live_component inside our modal renders the PicChatWeb.MessageLive.FormComponent LiveComponent.

This PicChatWeb.MessageLive.FormComponent renders the form_component.html.heex template because they have the same name, and the FormComponent does not define a render/1 function.


  <h2><%= @title %></h2>

  <.form
    let={f}
    for={@changeset}
    id="message-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">
  
    <%= label f, :content %>
    <%= textarea f, :content %>
    <%= error_tag f, :content %>
  
    <%= label f, :from %>
    <%= text_input f, :from %>
    <%= error_tag f, :from %>
  
    
      <%= submit "Save", phx_disable_with: "Saving..." %>
    
  

This form uses the phx-change binding to send the "validate" event whenever we modify a field in the form. The form also uses the phx-submit binding to send the "save" event when we submit the form.

These correspond to handlers defined in the FormComponent module. The "validate" handler uses the Chat.Message changeset to validate the form and display errors. The "save" handler attempts to save a message in the databasae.

def handle_event("validate", %{"message" => message_params}, socket) do
  changeset =
    socket.assigns.message
    |> Chat.change_message(message_params)
    # The changeset will not display errors unless there is an action
    # Usually, Ecto sets the action for us when we attempt to create the resource
    # However, in this case, we want to display errors before attempting to create the resource
    # See https://hexdocs.pm/ecto/Ecto.Changeset.html#module-changeset-actions for information
    |> Map.put(:action, :validate)

  {:noreply, assign(socket, :changeset, changeset)}
end

def handle_event("save", %{"message" => message_params}, socket) do
  save_message(socket, socket.assigns.action, message_params)
end

The save_message/3 function is called with the :new action, which triggers the following function definition: This either saves a message and redirects the user to http://localhost:4000 or returns a changeset with errors.

defp save_message(socket, :new, message_params) do
  case Chat.create_message(message_params) do
    {:ok, _message} ->
      {:noreply,
        socket
        |> put_flash(:info, "Message created successfully")
        # socket.assigns.return_to comes from the `return_to` attribute 
        # in our `<.live_component>`
        |> push_redirect(to: socket.assigns.return_to)}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, changeset: changeset)}
  end
end

Use the form to create a message. It will display on your list of messages.

Your Turn: Updating A Message

Updating a message walks through the exact same flow as creating a message.

Click the Edit button and you’ll be redirected to http://localhost:4000/1/edit to edit the message we created earlier.

Read through your code from the MessageLive.Index module as your code flows down to the FormComponent.

flowchart
MessageLive.Index --> mount --> handle_params --> apply_action --> index.html.heex --> modal[<.modal>] --> live_component[<.live_component>] --> FormComponent --> form.html.heex --> save[''save'' event] --> save_message/3

Notice two major differences. The MessageLive.Index.apply_action/3 is called with the :edit live_action and assigns a :message to the socket.

defp apply_action(socket, :edit, %{"id" => id}) do
  socket
  |> assign(:page_title, "Edit Message")
  |> assign(:message, Chat.get_message!(id))
end

Also, the FormComponent calls the save_message/3 function with the :edit live_action.

defp save_message(socket, :edit, message_params) do
  case Chat.update_message(socket.assigns.message, message_params) do
    {:ok, _message} ->
      {:noreply,
        socket
        |> put_flash(:info, "Message updated successfully")
        |> push_redirect(to: socket.assigns.return_to)}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, :changeset, changeset)}
  end
end

The MessageLive.Show LiveView

Visit http://localhost:4000/1 or click on the Show button to visit the MessageLive.Show LiveView.

The MessageLive.Show LiveView is simpler than MessageLive.Index because it only handles displaying and editing a message. On mount, it invokes the handle_params/3 function to assign the :page_title and :message fields to the socket.

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  @impl true
  def handle_params(%{"id" => id}, _, socket) do
    {:noreply,
     socket
     |> assign(:page_title, page_title(socket.assigns.live_action))
     |> assign(:message, Chat.get_message!(id))}
  end

  defp page_title(:show), do: "Show Message"
  defp page_title(:edit), do: "Edit Message"

This renders the show.html.heex template, which renders the content and buttons.

<ul>

  <li>
    <strong>Content:</strong>
    <%= @message.content %>
  </li>

  <li>
    <strong>From:</strong>
    <%= @message.from %>
  </li>

</ul>

<span><%= live_patch "Edit", to: Routes.message_show_path(@socket, :edit, @message), class: "button" %></span> |
<span><%= live_redirect "Back", to: Routes.message_index_path(@socket, :index) %></span>

Clicking the Edit button re-mounts the current liveview on http://localhost:4000/1/show/edit. This sets the live_action to :edit because of how we declared the live route in router.ex.

live "/:id/show/edit", MessageLive.Show, :edit

This sets the live_action field in our socket to display the modal.

<%= if @live_action in [:edit] do %>
  <.modal return_to={Routes.message_show_path(@socket, :show, @message)}>
    <.live_component
      module={PicChatWeb.MessageLive.FormComponent}
      id={@message.id}
      title={@page_title}
      action={@live_action}
      message={@message}
      return_to={Routes.message_show_path(@socket, :show, @message)}
    />
  
<% end %>

This is the exact same modal form we’ve already seen, so we’re not going to walk through it again.

Delete A Message

The MessageLive.Index LiveView also renders a delete button in the index.html.heex template.

<span><%= link "Delete", to: "#", phx_click: "delete", phx_value_id: message.id, data: [confirm: "Are you sure?"] %></span>

This button uses the phx_click and phx_value_id attributes which map to the phx-click and phx-value-id bindings. When clicked, this sends a "delete" message with the "id" parameter to our MessageLive.Index LiveView.

This triggers our handler in MessageLive.Index to delete the message and re-render the page with an updated list of messages.

def handle_event("delete", %{"id" => id}, socket) do
  message = Chat.get_message!(id)
  {:ok, _} = Chat.delete_message(message)

  {:noreply, assign(socket, :messages, list_messages())}
end

Your Turn: Tests

Read through the message_live_test.ex file to get a better understanding of patterns for testing LiveViews.

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

(Bonus) Further Study

A student who wants to do further research beyond this material may consider the following questions as inspiration.

  • What are slots?
  • How do you create a stateless function component?

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish PicChat: Messages reading"
$ git push

We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation

Home Report An Issue Math GamePicChat: Image Upload