(Partly generated by LLM)
Tutorial 2: A simple note taking app
In this tutorial, we’ll walk through the process of building a simple note-taking application using Phoenix LiveView. We’ll cover setting up the basic components, handling user interactions like showing and hiding modals, and performing CRUD (Create, Read, Update, Delete) operations on our notes.
Setting Up the Basic Components
First, we’ll implement a simple note-taking app. To get started, we need to define the routes that will handle our note-related actions. We’ll add two new live routes to our router.ex
file:
live "/notes", NoteLive, :index
live "/notes/new", NoteLive, :new
We’ll use a single render
function and leverage flags to conditionally render the “new note” page.
2.1 Showing and Hiding the Modal
To manage the state of our application, we’ll start by setting up the initial state in the mount
function. We’ll initialize an empty list of notes and a flag to control the visibility of our modal.
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:notes, [])
|> assign(:modal_open, false)
}
end
Since we’re using the same LiveView for both the index and new note pages, we need to handle the incoming parameters to differentiate between the two. The handle_params
function allows us to do this.
def handle_params(_params, _uri, socket) do
{:noreply, socket}
end
Our render
function will be responsible for displaying both the list of notes and the modal for creating a new note. The modal will be conditionally rendered based on the @live_action
assign, which is determined by the route.
def render(assigns) do
~H"""
<div>
<h3>Live action: {@live_action == :index}</h3>
<h1>Notes</h1>
<p><.link patch={~p"/notes/new"}>
<button class="bg-blue-500 text-white font-bold py-2 px-4 rounded">New Note</button></.link></p>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<%= for note <- @notes do %>
<tr>
<td><%= note.title %></td>
<td><%= note.content %></td>
<td>
<.link patch={~p"/notes/#{note.id}/edit"}>Edit</.link>
<button phx-click="delete" phx-value-id={note.id}>Delete</button>
</td>
</tr>
<% end %>
</tbody>
</table>
<div id="modalOverlay" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" :if={@live_action == :new} >
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" >
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" phx-click-away={close_modal()}>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h2>Modal Content</h2>
<p>This is a modal example.</p>
</div>
</div>
</div>
</div>
</div>
</div>
"""
end
The first part of this page displays a table of notes. When the “New Note” button is clicked, LiveView will patch the page to the /notes/new
route. As defined in our router, this will use the same NoteLive
module but with the :new
parameter. This parameter is passed as @live_action
in the assigns, which we use to conditionally render the modal.
To close the modal when the user clicks outside of it, we add the phx-click-away={close_modal()}
event handler. The close_modal
function then patches the URL back to /notes
, effectively closing the modal.
def close_modal() do
%JS{}
|> JS.patch(~p"/notes")
end
2.2 Saving and Canceling Data
The cancel functionality is already handled by our close_modal
function. Now, let’s create a form within the modal to add a new note. We’ll add a single field for the note’s title.
<div id="modalOverlay" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
:if={@live_action == :new} >
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" >
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" phx-click-away={close_modal()}>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h1 class="text-xl font-bold">
Create new note
</h1>
<form phx-submit="save-note" class="space-y-4">
<div>
<label for="title" class="block text-sm font-medium text-gray-700">Note</label>
<input type="text" name="title" id="title" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" />
</div>
<button type="submit" class="bg-blue-500 text-white font-bold py-2 px-4 rounded hover:bg-blue-600">Save</button>
</form>
</div>
</div>
</div>
</div>
</div>
We’ve also created an Ecto model for our notes with a single title
field. When the form is submitted, the save-note
event is triggered, and we can save the note using Ecto.
def handle_event("save-note", %{"title" => title}, socket) do
case NoteApp.create_note(%{title: title}) do
{:ok, _note} ->
{:noreply,
socket
|> assign(:notes, NoteApp.list_notes())
|> push_patch(to: ~p"/notes")
}
{:error, changeset} ->
{:noreply, socket |> assign(:modal_open, true)}
end
end
2.3 Showing Note Details
To show the details of a note, we’ll first add a new route to our router.ex
file.
live "/notes/:id/show", NoteLive, :show
Next, we’ll modify the template to include a link to the note’s title, which will trigger the :show
action.
<td class="px-6 py-4 text-center text-sm text-gray-900">
<.link patch={~p"/notes/#{note.id}/show"} class="text-blue-600 hover:text-blue-800">
<%= note.title %>
</.link>
</td>
We’ll also add a conditional modal to display the note’s details.
<div id="noteDetailsModal" class="fixed inset-0 z-50 bg-gray-500 bg-opacity-75 transition-opacity"
:if={@live_action in [:show]}>
<div class="flex items-center justify-center min-h-screen">
<div class="bg-white rounded-lg shadow-lg p-6 max-w-md w-full" phx-click-away={close_modal()}>
<h2 class="text-2xl font-bold mb-4">Note Details</h2>
<p class="text-lg font-semibold">
Title: <span id="noteTitle">{@note.title}</span>
</p>
<p class="text-sm text-gray-600">
Created At: <span id="noteCreatedAt">{@note.inserted_at}</span>
</p>
<p class="text-sm text-gray-600">
Updated At: <span id="noteUpdatedAt">{@note.updated_at}</span>
</p>
</div>
</div>
</div>
2.4 Editing Note Details
To edit a note, we’ll start by modifying the template to handle both the :new
and :edit
actions in the same modal.
<div id="modalOverlay" class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
- :if={@live_action == :new} >
+ :if={@live_action in [:new, :edit]} >
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" >
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen"></span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full" phx-click-away={close_modal()}>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<%!-- <h2>Modal Content</h2> --%>
<h1 class="text-xl font-bold">
- Create new note
+ {if @live_action == :new, do: "Create new note", else: "Edit note"}
</h1>
<%!-- <p>This is a modal example.</p> --%>
<form phx-submit="save-note" class="space-y-4">
<div>
+ <input type="hidden"
+ name="note_id"
+ value={@note.id}
+ :if={@note}
+ />
<label for="title" class="block text-sm font-medium text-gray-700">Note</label>
- <input type="text" name="title" id="title" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500"/>
+ <input type="text" name="title" id="title" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500" value={@note && @note.title || ""}/>
</div>
<button type="submit" class="bg-blue-500 text-white font-bold py-2 px-4 rounded hover:bg-blue-600">Save</button>
</form>
</div>
</div>
</div>
</div>
</div>
Next, we’ll add a new route to our router.ex
file to handle the edit action.
live "/notes/new", NoteLive, :new
+ live "/notes/:id/edit", NoteLive, :edit
live "/notes/:id/show", NoteLive, :show
We’ll also add a new handle_event
function to update the note when the note_id
is present in the event parameters.
+ def handle_event("save-note", %{"note_id" => id, "title" => title}, socket) do
+ NoteApp.update_note(
+ NoteApp.get_note!(id),
+ %{title: title}
+ )
+
+ {:noreply,
+ socket
+ |> assign(:notes, NoteApp.list_notes())
+ |> push_patch(to: ~p"/notes")
+ }
+
+ end
To ensure that the note is cleared when we navigate back to the index route, we’ll modify the handle_params
function.
def handle_params(params, uri, socket) do
case params do
%{"id" => id} ->
note = NoteApp.get_note!(id)
IO.inspect(note, label: "Fetched note in handle_params")
{:noreply, assign(socket, :note, note)}
+ _ ->
+ {:noreply, assign(socket, :note, nil)}
end
end
Finally, here’s the Ecto function to update a note.
def update_note(%Note{} = note, attrs) do
note
|> Note.changeset(attrs)
|> Repo.update()
end
And that’s it! We’ve built a simple yet functional note-taking application with Phoenix LiveView, covering the essential concepts of handling user interactions and data manipulation.