...
-
"""
end
Now we're all set to handle the events we defined earlier!
Fly.io [?] Elixir
Fly.io is a great way to run your Phoenix LiveView app close to your
users. It's really easy to get started. You can be running in
minutes.
Deploy a Phoenix app today! -
[cta-rabbit]
Add, Delete, Update, Reset Streams
We'll be defining five events (new, validate, save, delete, reset).
Our main focus is explain how we can use streams to reflect changes
in our list. We won't be diving into functions that interact with the
database.
To get started, it will be helpful to define a helper function that
creates forms already associated with the list_id:
defp build_empty_form(list_id) do
build_item_form(%Item{list_id: list_id}, %{})
end
Keep in mind this function, as well as the one we defined earlier.
We'll be using them in our upcoming steps!
New
To add a new element to the list, we can use the function we just
defined to create an empty form. Then, we can insert this form into
the stream using the stream_insert/4 function:
def handle_event("new", %{"list_id" => list_id}, socket) do
{:noreply, stream_insert(socket, :items, build_empty_form(list_id), at: -1)}
end
To insert the new form at the end of the list, we use the :at option
provided by the stream_insert/4 function.
Validate
To display the errors of our item, we need to modify the already
rendered form in the client and insert a new form that includes the
errors from the changeset.
One important detail to note is that in order for our component to
recognize that it should display the changeset errors, we need to add
an :action to it. To achieve this, we make a slight modification to
the build_item_form/4 function that we defined earlier:
-defp build_item_form(item_or_changeset, params) do
+defp build_item_form(item_or_changeset, params, action \\ nil) do
changeset =
item_or_changeset
|> SortableList.change_item(params)
+ |> Map.put(:action, action)
to_form(changeset, id: "form-#{changeset.data.list_id}-#{changeset.data.id}")
end
The action can be any random atom, but hey, let's keep things clear
and name them sensibly.
Now let's see how to handle the event and use this new :action
parameter:
def handle_event("validate", %{"item" => item_params} = params, socket) do
item = %Item{id: params["id"] || nil, list_id: item_params["list_id"]}
item_form = build_item_form(item, item_params, :validate))
{:noreply, stream_insert(socket, :items, item_form}
end
First, we generate a new %Item{} that includes the item id sent from
the text input and the list_id sent using the phx-value-id=
{form.data.id} parameter. Next, we call our build_item_form/4
function with the :validate action and insert the item into the
stream using stream_insert/4.
Like magic, we didn't have to specify that this is an update of a
form that already exists! This is because to_form created a DOM ID
that allows to identify the elements of the stream that already
exists in the client.
Save
When attempting to save a new item, we may receive one of two
possible responses. First, when an item is successfully inserted, we
clear the new item form and replace it with a fresh empty form. We
also insert a new form containing the persisted data. Second, if an
error occurs, we display the relevant errors to the user.
Let's now delve into the implementation details of these actions.
def handle_event("save", %{"item" => item_params}, socket) do
case SortableList.create_item(item_params) do
{:ok, new_item} ->
empty_form = build_empty_form(item_params["list_id"])
{:noreply,
socket
|> stream_insert(:items, build_item_form(new_item, %{}))
|> stream_delete(:items, empty_form)
|> stream_insert(:items, empty_form)}
{:error, changeset} ->
{:noreply, assign(socket, :form, build_item_form(changeset, %{}, :insert)))}
end
end
Great! We can chain together the different functions of the stream
using the pipeline operator.
On the other hand, the save event may be triggered by one of the
forms that have already been saved in the database, indicating an
update of the item. We can identify this by pattern matching,
receiving a non-null item_id:
def handle_event("save", %{"id" => item_id, "item" => params}, socket) do
todo = SortableList.get_item!(item_id)
case SortableList.update_item(todo, params) do
{:ok, updated_item} ->
{:noreply, stream_insert(socket, :items, build_item_form(updated_item, %{}))}
{:error, changeset} ->
{:noreply, stream_insert(socket, :items, build_item_form(changeset, %{}, :update))}
end
end
It's almost like magic, isn't it? We just need to use stream_insert,
and the stream takes care of updating itself. And if we want to
display the errors from the changeset, we simply add an :action to
it.
Delete
Deleting items from a stream is easy too!
For this we have stream_delete/3 which also receives changesets as a
parameter:
def handle_event("delete", %{"id" => item_id}, socket) do
item = SortableList.get_item!(item_id)
{:ok, _} = SortableList.delete_item(item)
{:noreply, stream_delete(socket, :items, build_item_form(item, %{}))}
end
You can also use the stream_delete/3 function to discard the form of
the new element when needed:
def handle_event("discard", params, socket) do
{:noreply, stream_delete(socket, :items, build_empty_form(params["list_id"]))}
end
Reset
We have the option to remove all items from a stream, which is
perfect for our reset button:
def handle_event("reset", params, socket) do
empty_form = build_empty_form(params["list_id"])
{:noreply, stream(socket, :items, [empty_form], reset: true)}
end
We simply need to reconfigure our stream by passing the option reset:
true. Additionally, we can include a list of new elements to be
inserted, which in this case would be at least one empty form.
LiveView 0.19 added support for stream resets with bulk insert
operations!
Hooray! We've done it! Our component is now complete and ready to
shine!
Closing
The features we explored today unlock a world of exciting new
possibilities for apps development with LiveView. LiveView Streams
revolutionize collection handling and memory optimization,
simplifying tasks such as adding, editing, and deleting items.
Furthermore, the optimizations brought by to_form/1 enable efficient
manipulation of individual inputs without the need for full form
re-rendering. This simple yet immensely powerful function opens up
new avenues for form usage, expanding the potential of your
applications.
Check out this repo to see these game-changing features in action. We
used our previous learnings to create an even more impressive
component!
Credits
A big shoutout to Chris McCord for sharing the incredible example
that inspired these posts and for patiently answering any questions
about the exciting new concepts in Phoenix and LiveView.
Last updated *
May 29, 2023
Share this post on Twitter Share this post on Hacker News Share this
post on Reddit
Author
Berenice Medel
Name
Berenice Medel
Social Media
@bemesa21 View Twitter Profile
Previous post |
Taking Control of Map Sort Order in Elixir
Previous post |
Taking Control of Map Sort Order in Elixir
App performance optimization
Company
About Pricing Jobs
Articles
Blog Phoenix Files Laravel Bytes Ruby Dispatch Django Beats
Resources
Docs Support Status
Contact
GitHub Twitter Community
Legal
Security Privacy policy Terms of service
Copyright (c) 2023 Fly.io