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.exfile 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
liveis a macro that requires 3 arguments, in this case, we are passing 2.Endpoint URL
Module name of the
liveviewfile associated with that endpoint.
So, we are going to have 3 modules in the folder
lib/note_app_web/live. They arenotes_index_live.exnotes_new_live.exnotes_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
livefile starts with themountfunction which acts like a constructor (in OOP sense). It takes 3 parametersparams=>paramshas URL parameter specific data.session=> User session specific information.socket=> It acts as astatefor every Liveview page.
It returns the
oktuple with thesocket.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 notesandnumber of notesto 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
aliastheNoteServermodule i.e GenServer. If you remember there is anall_notesfunction in it. We have called it and assigned it to theall_notesvariable.Using the
assignfunction we have assigned thesenotesto thenotesvariable.Similarly, we have assigned the number of notes to the
notes_lengthvariable.Now our data is ready, now we need to render the view.
For that Liveview has a
renderfunction. 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">☶ 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.cssfile in the project repo of this blog.Rest displaying the logic of the list of notes is simple, we are just iterating over the
@notesvariable and displaying it under thenotes-list-itemdiv.There is
live_patchhelperdefined twice in the view. One is used to navigate to the detail page of the particular note. The other navigate to thecreatepage.This function takes the
Route.live_pathvalue which says where we are going to navigate and its associated params.So we are passing the
NotesEditLiveandNotesNewLiveas 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
updateanddeletein the view.We have these two methods in our
NoteServermodule as well.If you remember we added
live_patchlink to naviate to the details page. We passedRoutes.live_path(@socket, NotesEditLive, note.id)to thelive_patchhelper.The
note.idin thelive_pathfunction is nothing but path param for the detail page which generates URL something like thisnotes/1/edit.Inside the
detailsLiveView mount function we are going to fetch thisidfrom theparamsparameter of the mount function.Using this
idwe are going to fetch a particular note.In file
lib/note_app_web/live/notes_new_live.exwe are going to add followingmountfunction.
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
renderfunction. There we have added the form with somehandle_eventslikephx-changeto manage change in the inputs andphx-submitto submit the form. (Check References for these handle_events)We have added the
Deletebutton with thephx-clickevent 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-submitevent that gets called on clicking theDonebutton, 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
/newendpoint is very similar to the details page.We are navigating to this page by passing
NotesNewLivemodule tolive_path.Routes.live_path(@socket, NotesNewLive)In this LiveView module we are going to initialize empty variables
titleandbodyfor 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_notefunction 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.




