Creating Note taking app using LiveView and GenServer - Part 2

Creating Note taking app using LiveView and GenServer - Part 2

This blog is a continuation of a two-part blog. This is going to be part 2. In the previous blog, we've seen the designing of the process layer (Boundary Layer) of the application. We implemented the GenServer and started it with Supervisor. We also tested the API layer in iex shell and it is working as per our expectations. In this blog, we are going to implement the UI part of the application using Phoenix Liveview.

Plan of attack

  • We are going to add different LiveView routes ✓
  • We will implement List page displaying all notes ✓
  • Implement detail page for note ✓
  • Add update and delete action for the note ✓
  • Add create note form ✓
  • We will be integrating the API with each of these actions. ✓

Let's start adding routes definition 🚧

  • As discussed we are going to have 3 routes.
  • One for "List", "detail" and one for "create".
  • For adding route open lib/note_app_web/router.ex file and add following code.

    scope "/", NoteAppWeb do
      pipe_through :browser
    
      # These are the new routes
      live "/", NotesIndexLive
      live "/notes/new", NotesNewLive
      live "/notes/:id/edit", NotesEditLive
    end
    
  • live is a macro that requires 3 arguments, in this case, we are passing 2.
    1. Endpoint URL
    2. Module name of the liveview file associated with that endpoint.
  • So, we are going to have 3 modules in the folder lib/note_app_web/live. They are
    1. notes_index_live.ex
    2. notes_new_live.ex
    3. notes_edit_live.ex.
  • These routes are self-explanatory as per the name.

Add Notes List page ✅

  • Every live module acts like a controller, which is invoked as soon as we hit the route associated with that live file.
  • Every live file starts with the mount function which acts like a constructor (in OOP sense). It takes 3 parameters
    • params =>params has URL parameter specific data.
    • session => User session specific information.
    • socket => It acts as a state for every Liveview page.
  • It returns the ok tuple with the socket.
  • Since, this page is about the list of notes, we are going to query our GenServer about the list of notes it has.
  • We are going to assign queried list of notes and number of notes to the socket variable.

    defmodule NoteAppWeb.NotesIndexLive do
      use NoteAppWeb, :live_view
      alias NoteApp.Notes.NoteServer
    
      def mount(_params, _session, socket) do
         all_notes = NoteServer.all_notes()
         socket = assign(socket, :notes, all_notes)
         socket = assign(socket, :notes_length, Kernel.length(all_notes))
         {:ok, socket}
      end
    end
    
  • As you will notice we have alias the NoteServer module i.e GenServer. If you remember there is an all_notes function in it. We have called it and assigned it to the all_notes variable.
  • Using the assign function we have assigned these notes to the notes variable.
  • Similarly, we have assigned the number of notes to the notes_length variable.
  • Now our data is ready, now we need to render the view.
  • For that Liveview has a render function. We have our list of notes and their count we are going to display it in this render function.

     ~L"""
        <div class="notes">
          <div class="notes-header">
              <h2 class="notes-title">&#9782; Notes</h2>
              <p class="notes-count"><%= @notes_length %></p>
          </div>
    
          <div class="notes-list">
            <%= for note <- @notes do %>
              <%= live_patch to: Routes.live_path(@socket, NotesEditLive, note.id) do %>
                <div class="notes-list-item">
                  <h3><%= note.title %></h3>
                  <p><%= note.body %></p>
                </div>
              <% end %>
            <% end %>
          </div>
    
          <%= live_patch "+", to: Routes.live_path(@socket, NotesNewLive), class: "floating-button" %>
        </div >
      """
    
  • This HTML code has lots of styling which you can copy from the assets/app.css file in the project repo of this blog.
  • Rest displaying the logic of the list of notes is simple, we are just iterating over the @notes variable and displaying it under the notes-list-item div.
  • There is live_patch helper defined twice in the view. One is used to navigate to the detail page of the particular note. The other navigate to the create page.
  • This function takes the Route.live_path value which says where we are going to navigate and its associated params.
  • So we are passing the NotesEditLive and NotesNewLive as value to these helpers.

Add detail page for note along with edit ✄ and delete ✕ action

  • Detail page is where we are going to display detail of the note title and its body.
  • We have a separate LiveView file for it as well i.e NotesEditLive.
  • We are going to add two actions update and delete in the view.
  • We have these two methods in our NoteServer module as well.
  • If you remember we added live_patch link to naviate to the details page. We passed Routes.live_path(@socket, NotesEditLive, note.id) to the live_patch helper.
  • The note.id in the live_path function is nothing but path param for the detail page which generates URL something like this notes/1/edit.
  • Inside the details LiveView mount function we are going to fetch this id from the params parameter of the mount function.
  • Using this id we are going to fetch a particular note.
  • In file lib/note_app_web/live/notes_new_live.ex we are going to add following mount function.

    defmodule NoteAppWeb.NotesEditLive do
      alias NoteAppWeb.NotesIndexLive
      use NoteAppWeb, :live_view
      alias NoteApp.Notes.NoteServer
    
      def mount(%{"id" => id}, _session, socket) do
         note = NoteServer.get_note(String.to_integer(id))
        {:ok, assign(socket, :note, note)}
      end
    
  • Similarly, we are going to add our view in the render function. There we have added the form with some handle_events like phx-change to manage change in the inputs and phx-submit to submit the form. (Check References for these handle_events)
  • We have added the Delete button with the phx-click event which handles the logic to delete the note.
        def handle_event("delete_note", _value, socket) do
             NoteServer.delete_note(socket.assigns.note.id)
             {:noreply,
                socket
                |> redirect(to: "/")
             }
        end
    
  • Finally, we have phx-submit event that gets called on clicking the Done button, and logic for updating the note is called.
       def handle_event("save_note", _value, socket) do
             NoteServer.update_note(socket.assigns.note)
            {:noreply,
               socket
               |> redirect(to: "/")
             }
        end
    
  • The final working code for the Notes detail Liveview is below.

    defmodule NoteAppWeb.NotesEditLive do
      alias NoteAppWeb.NotesIndexLive
      use NoteAppWeb, :live_view
      alias NoteApp.Notes.NoteServer
    
      def mount(%{"id" => id}, _session, socket) do
         note = NoteServer.get_note(String.to_integer(id))
        {:ok, assign(socket, :note, note)}
      end
    
      def render(assigns) do
        ~L"""
          <div class="note">
              <div class="note-header">
                 <h3>
                      <%= live_patch "<", to: Routes.live_path(@socket, NotesIndexLive) %>
                 </h3>
                 <button type="submit" form="create-note-form">Done</button>
                 <button phx-click="delete_note">Delete</button>
               </div>
    
               <form id="create-note-form" phx-change="detect_change" phx-submit="save_note">
                  <input type="text" placeholder="Add Title" value="<%= @note.title %>" name="title"/>
                   <br>
                   <br>
                   <textarea placeholder="Add note" name="body"><%= @note.body %></textarea>
               </form>
            </div >
         """
        end
    
        def handle_event("detect_change", %{"title" => title, "body" => body}, socket) do
            updated_note = %{socket.assigns.note | title: title, body: body}
           {:noreply, assign(socket, :note, updated_note)}
        end
    
        def handle_event("save_note", _value, socket) do
             NoteServer.update_note(socket.assigns.note)
            {:noreply,
               socket
               |> redirect(to: "/")
             }
        end
    
        def handle_event("delete_note", _value, socket) do
             NoteServer.delete_note(socket.assigns.note.id)
             {:noreply,
                socket
                |> redirect(to: "/")
             }
        end
    end
    

Add page for creating a new note

  • The only part that is remaining is the creation of note.
  • The form on the /new endpoint is very similar to the details page.
  • We are navigating to this page by passing NotesNewLive module to live_path. Routes.live_path(@socket, NotesNewLive)
  • In this LiveView module we are going to initialize empty variables title and body for the form and will use them in the render function.
  • Similar to the edit page we will have handle_events on saving the form where we are going to call the create_note function from our GenServer API.

    defmodule NoteAppWeb.NotesNewLive do
    use NoteAppWeb, :live_view
    alias NoteApp.Notes.NoteServer
    alias NoteAppWeb.NotesIndexLive
    
    def mount(_params, _session, socket) do
      socket = assign(socket, title: "", body: "")
      {:ok, socket}
    end
    
    def render(assigns) do
      ~L"""
      <div class="note">
        <div class="note-header">
          <h3>
            <%= live_patch "<", to: Routes.live_path(@socket, NotesIndexLive) %>
          </h3>
          <button type="submit" form="create-note-form">Done</button>
        </div>
    
        <form id="create-note-form" phx-change="detect_change" phx-submit="save_note">
          <input type="text" placeholder="Add Title" value="<%= @title %>" name="title"/>
          <br>
          <br>
          <textarea placeholder="Add note" name="body"><%= @body %></textarea>
        </form>
      </div >
      """
    end
    
    def handle_event("detect_change", %{"title" => title, "body" => body}, socket) do
      {:noreply, assign(socket, title: title, body: body)}
    end
    
    def handle_event("save_note", _value, socket) do
      new_note = %{id: nil, title: socket.assigns.title, body: socket.assigns.body}
      NoteServer.create_note(new_note)
      {:noreply,
           socket
           |> redirect(to: "/")
      }
    end
    end
    

So, that's our complete LiveView implementation of all CRUD functions. There is some scope of refactoring in the app like creating components for common code (which I will cover in another blog 😉). The main purpose of the blog was to introduce the concept of GenServer and LiveView. I hope you like this blog. If you have any questions then please comment below.

References:

Did you find this article valuable?

Support AbulAsar S. by becoming a sponsor. Any amount is appreciated!