Skip to main content

Command Palette

Search for a command to run...

Creating Note taking app using LiveView and GenServer - Part 2

Updated
β€’7 min read
Creating Note taking app using LiveView and GenServer - Part 2
A
I am a Software Engineer from Mumbai, India. In love with Functional programming ( precisely Elixir). Love to share the knowledge that I learn while developing things.

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:

More from this blog

Abul Asar's Blog

32 posts