Creating Note taking app using LiveView and GenServer - Part 2

Search for a command to run...

No comments yet. Be the first to comment.
Motivation Recently, I tried to learn some low-level system programming stuff. I am a Mac user, and I thought that everything that works on Linux should also work on Mac. After all, Mac is a Unix-based system 😊. I guess we all heard this. Oh boy! I ...

Problem Statement While working on a Nestjs project, I encountered a weird problem related to the database column. I was trying to insert a record into a MySQL table using TypeORM. The error I was experiencing stated that a specific column “cannot be...

Recently, I was exploring design patterns courses on my LinkedIn Learning subscription. I came across a course, Node.js: Design Patterns by Alex Banks. It is a wonderful, easy-to-understand course. I started with the Builder Pattern, and the explanat...

Suppose you are working on a table in a LiveView project. This table has limited static data of not more than one page (you can avoid questions about pagination in the comment section 😊). From a user's point of view, it becomes hard to look into the...

Recently, while working on one of my personal Elixir projects. I came across a scenario where I needed to make some changes to the database schema.The changes mainly revolve around adding and removing indexes due to changes in the business requiremen...

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.
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. ✓
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 are
notes_index_live.ex
notes_new_live.ex
notes_edit_live.ex.
These routes are self-explanatory as per the name.
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">☶ 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.
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
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
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
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.