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.- Endpoint URL
- 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 arenotes_index_live.ex
notes_new_live.ex
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 themount
function which acts like a constructor (in OOP sense). It takes 3 parametersparams
=>params
has URL parameter specific data.session
=> User session specific information.socket
=> It acts as astate
for every Liveview page.
- It returns the
ok
tuple 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 notes
andnumber 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
theNoteServer
module i.e GenServer. If you remember there is anall_notes
function in it. We have called it and assigned it to theall_notes
variable. - Using the
assign
function we have assigned thesenotes
to thenotes
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">☶ 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 thenotes-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 thecreate
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
andNotesNewLive
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
anddelete
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 passedRoutes.live_path(@socket, NotesEditLive, note.id)
to thelive_patch
helper. - The
note.id
in thelive_path
function is nothing but path param for the detail page which generates URL something like thisnotes/1/edit
. - Inside the
details
LiveView mount function we are going to fetch thisid
from theparams
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 followingmount
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 somehandle_events
likephx-change
to manage change in the inputs andphx-submit
to submit the form. (Check References for these handle_events) - We have added the
Delete
button with thephx-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 theDone
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 tolive_path
.Routes.live_path(@socket, NotesNewLive)
- In this LiveView module we are going to initialize empty variables
title
andbody
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.