ðAdding dynamic behavior
You may notice that viewing the to-do list may be nice, but it is sorely lacking the key features of a to-do application like creating a new to-do and marking a to-do as done.
If you have dug around with the original Phoenix documentation, you will notice that there is no mention on how this can be achieved. This is because in the base version of Phoenix, handling button actions and performing such dynamic changes requires the use of JavaScript. This is not uncommon for most server-side frameworks.
"But I don't want to use JavaScript!" - Presumably you
This is where Phoenix LiveView comes in. Phoenix LiveView is an "add-on" on top of base Phoenix that provides real-time server-rendered HTML.
The following content covered will require a bit of time to grasp as it is a relatively new concept introduced over this guide. Do take your time when reading and understanding.
Adding buttons
Before we dive into LiveView, there are some changes to the design that we will add such as buttons for the actions and a task bar to add new to-do items.
Replace the todo.html.heex
file with:
And edit the todo_item
functional component:
You should see the following UI once done:
Introducing LiveView
Phoenix LiveView inverts the traditional request-response lifecycle that we have seen so far. Instead of sending a request to the API/server and re-rendering the front-end based on the response, LiveView first uses a regular HTTP request-response to retrieve the initial page.
The key benefit from doing this is reducing the amount of time spent waiting for HTTP responses in a traditionally stateless application.
However, once the page is rendered, a persistent connection (via a socket) is established between client and server and this connection is used to communicate any changes/actions performed from the front-end to the back-end and vice versa.
This gives rise to the following lifecycle for LiveView (courtesy of John Elm Labs for this diagram):
It may seem like a handful but we will only be focusing on the components colored orange as they are the most fundamental ideas of LiveView.
Getting started with LiveView
If you wish to get the converted version of the LiveView application immediately without running through all of the steps to convert the current demo application to LiveView, you can pull the branch liveview-base
from the GitHub repository and get started.
The full demo application is found on the liveview
branch.
Otherwise, you can expand the section below to learn how to convert a base Phoenix project to use LiveView.
Whether you have pulled the branch liveview-base
or followed the guide in the expandable above, you will notice that there are several changes to the structure of the project:
Adding
lib/practical_elixir_demo_web/live/
withtodo_live.ex
andtodo_live.html.heex
: these are just to create a logical separation between LiveView and base Phoenix, though most projects will adopt a similar separationConverting
get "/todo"...
withlive "/todo"
: this is a key part of declaring the/todo
route as being a LiveView; we can omit the specific controller action as the entire LiveView will be treated as the controller + viewDeclaring the data retrieval in
mount/3
oftodo_live.ex
: as per the new lifecycle of LiveView, themount/3
function is called before the page is rendered so we will use it to load the initial data (i.e. to-do list)You may also notice that we call
assign
in themount/3
function while passing the socket and a keyword list of values.assign
is used to persist the state of the page in the persistent connection (socket). It is what allows us to interact with the client without having to use HTTP calls!
Everything else is the same as the previous code we had written! That's one of the biggest draws of Phoenix LiveView - a high degree of interoperability with base Phoenix!
Creating new to-dos
Let's make the "Add" button functional. First, notice that we have defined a form
element in todo_live.html.heex
and within the form
element, we have included an attribute: phx-submit
. This creates a binding between the client and server.
What this binding does is inform Phoenix that on form submission (via the button in the form), Phoenix should trigger the given event. Then, you must specify the appropriate event handler in the LiveView controller:
As highlighted previously (in Introducing LiveView), handle_event/3
is one of many callbacks that the server uses to handle when a client event is triggered. The event is comprised of three parameters:
Event name: name of the event raised by the client (this is the name that is given to
phx-submit
)Event payload: values given to the server from the client, usually the form data
Existing socket: represents the persistent connection, updating the data stored in this socket informs the client to update its view to render the latest information as well
With this understanding, we can now implement the to-do item creation logic.
The first thing we want to do is to retrieve the existing list of to-dos from the socket. This can be done using Pattern matching since both the the value
and socket
are maps.
In this demonstration, we have opted to keep things simple and remove the use of the to-do description field. However, if you wish to extend the application, feel free to add support for filling in that field as well!
Then, we can update the to-do list with the new task and submit the update to the client via the socket.
We're literally just adding a new TodoItem
to the front of the todo_list
and re-assigning it to the socket. When you try out this interaction on your application, you will see the new task added to the very top of the to-do list.
Therein lies one of the strengths of Phoenix LiveView: the ability to use handle client-server interaction directly as Elixir functions, rather than HTTP calls.
:noreply
tells the client that there is no additional information to be sent to the client. :reply
exists to indicate that there is some reply to be sent to the client.
For more information, refer to the documentation of handle_event/3
Marking to-dos as done
Another common action in a to-do application would be to mark your tasks as done. We can implement such logic like we have for the previous feature. However, this time, we will need to update several fields of the HEEx file to make this a reality.
The most important step is to identify the relative position of each to-do item within the list. This can be done by altering how we generate each to-do item, adding an id
to each of them representing their unique position:
We use a helper function Enum.with_index/2
to include the relative index of each to-do item. Then, we pass the id
to the todo_item
functional component. In the controller, we need to also update the functional component to make use of this id
:
We use the phx-value-*
binding to specify this as a parameter. This means that when we perform an event on the button, this value would be sent as part of the values
map that the handle_event/3
callback receives.
Speaking of which, we should also add a binding to handle the button clicks as well. We use the phx-click
binding for this:
We have deliberately made these two separate events but you can very well use one event to handle both cases (we will leave this up to you to figure it out!). Finally, we can implement the behavior of the buttons via these events:
Here, we are using Enum.with_index/1
and Enum.map/2
to implement the update logic. Since state is immutable in Elixir, we have to re-create the to-do list with the is_done?
field of the target to-do item changed to the appropriate value.
Now, if you ran the application, you will be able to add new to-do items and change their completion status:
What's next?
For editing, you can try making the title editable. For deletion, use a similar logic as updating the "done" status of the to-do items.
However, if you refresh the page, all of your to-dos will disappear! That's not good... This is where data persistence comes in. As mentioned in the introduction chapter, the next chapter is entirely optional but highly recommended for you to gain a holistic understanding of using Phoenix for web development (since data persistence is basically a must-have in production applications!)
Last updated