Now Reading
Dynamic types with LiveView Streams · Fly

Dynamic types with LiveView Streams · Fly

2023-05-30 01:52:08

Picture by Annie Ruygt

On this submit, we’ll develop a dynamic listing part utilizing the brand new LiveView Streams and the improved type options launched in LiveView 0.18/0.19. Prepare to find the right way to create a responsive listing part with interactive capabilities like including, enhancing, and deleting gadgets. Fly.io is a superb place to run your Phoenix LiveView purposes! Try the right way to get started!

You developed a part that permits you to add and take away gadgets in a “has_many” relationship. Take, for instance, a buying listing the place it will probably have many gadgets. You’ve got bought a cool type that permits you to ship information for brand new gadgets, and it really works rather well:

Nonetheless, you are still not fully proud of the consequence. Whenever you make a mistake whereas including an merchandise to the listing, there isn’t any solution to edit it. How will you make the listing gadgets editable proper within the listing part, with out the necessity for a modal?

Nicely, what if we use particular person types for every listing merchandise as a substitute of a single type so as to add and affiliate them with the listing? That is a easy process, we will render as many types as we wish. We’ll use the to_form/2 perform to create type structs from changesets, after which we will use these structs to render our type elements.

And one other factor: as soon as we have fastened up our type part, we will see the right way to use LiveView Streams to handle and manipulate our listing gadgets with out the necessity to retailer all of them in reminiscence. Including, enhancing, deleting, and resetting gadgets in a set has by no means been simpler!

Defining Our Component’s Markup

Let’s begin by refactoring our component and holding solely the header:

defmodule ComponentsExamplesWeb.ListComponent do
  use ComponentsExamplesWeb, :live_component

  def render(assigns) do
    ~H"""
    <div class="bg-grey-100 py-4 rounded-lg">
      <div class="area-y-5 mx-auto max-w-7xl px-4 area-y-4">
        <.header>
          <%= @list_name %>
        </.header>
      </div>
    </div>
    """
  finish

  def replace(%{listing: listing} = assigns, socket) do
    socket =
      socket
      |> assign(
        list_id: listing.id,
        list_name: listing.title
      )
    {:okay, socket}
  finish
finish  

Within the assigns, we’re passing a struct listing. It comprises an id, a title, and an inventory of things, every of which has a number of attributes.

To generate these structs, their corresponding changesets, and the context features that we’ll be utilizing later, you should use the phx.gen.context generator:

combine phx.gen.context SortableList Record lists title:string
combine phx.gen.context SortableList Merchandise gadgets identify:string place:integer list_id:integer

This is an instance listing, containing a single merchandise:

%ComponentsExamples.SortableList.Record{
  __meta__: #Ecto.Schema.Metadata<:loaded, "lists">,
  id: 1,
  title: "Procuring listing 1",
  gadgets: [
    %ComponentsExamples.SortableList.Item{
      __meta__: #Ecto.Schema.Metadata<:loaded, "items">,
      id: 161,
      name: "chocolate",
      position: 0,
      status: :started,
      list_id: 1,
      list: #Ecto.Association.NotLoaded<association :list is not loaded>,
      inserted_at: ~N[2023-05-16 20:29:12],
      updated_at: ~N[2023-05-16 20:29:12]
    }
  ],
  inserted_at: ~N[2023-04-25 19:35:09],
  updated_at: ~N[2023-04-25 19:35:09]
}

Now, we have to iterate over the gadgets of the listing and render the types. However earlier than we dive into that, let’s replace our assigns to incorporate a type per merchandise:

def replace(%{listing: listing} = assigns, socket) do
+ item_forms = Enum.map(listing.gadgets, &build_item_form(&1, %{list_id: listing.id}))

  socket =
    socket
    |> assign(
      list_id: listing.id,
      list_name: listing.title,
+     gadgets: item_forms
    )
  {:okay, socket}
finish

Let’s have a look at the content material of the build_item_form/1 perform:

defp build_item_form(item_or_changeset, params) do
  changeset =
    item_or_changeset
    |> SortableList.change_item(params)

  to_form(changeset, id: "form-#{changeset.information.list_id}-#{changeset.information.id}")
finish

We obtain an %Merchandise{} and create a changeset from it. Then, we use the Phoenix.Component.html.to_form/2 perform. This helpful perform converts a knowledge construction right into a Phoenix.HTML.Form, which we will use to render our form/1 elements. Notice that the id of the shape has the list_id and the id of the merchandise interpolated.

Because of this, we now have an assign referred to as :gadgets that holds the Phoenix.HTML.Kind structs containing our component information. We iterate by way of every merchandise in our assign and render a simple_form/1 for every of them:

def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>
        <%= @list_name %>
      </.header>
+     <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
+      <div :for={type <- @gadgets} id={"listing#{@list_id}-item#{merchandise.id}"}>
+       <.simple_form
+           for={type}
+           phx-change="validate"
+           phx-submit="save"
+           phx-target={@myself}
+           class="min-w-0 flex-1 drag-ghost:opacity-0"
+           phx-value-id={type.information.id}
+         >
+          <div class="flex">
+          </div>
+         </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
finish

We outline the occasions triggered on change or information submission, and we go the merchandise ID to those occasions utilizing the phx-value-* bindings.

Now, let’s add components to the physique of our merchandise type. We begin by including a button to vary the standing of our component:

This is a screenshot of the shopping list component. In the screenshot, there is a button highlighted by a red square. This button can be used to toggle the status of an item in the list.

def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={type <- @gadgets} id={"listing#{@list_id}-item#{merchandise.id}"}>
          <.simple_form ...>
            <div class="flex">
+.            <button
+               :if={type.information.id}
+               kind="button"
+               class="w-10"
+.              phx-click={JS.push("toggle_complete", goal: @myself, worth: %{id: type.information.id})}
+             >
+               <.icon
+                 identify="hero-check-circle"
+                 class={[
+                   "w-7 h-7",
+.                  if(form[:status].worth == :accomplished, 
+                     do: "bg-green-600", 
+                     else: "bg-gray-300")
+                 ]}
+               />
+.            </button>
            </div>
          </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
finish

This button is displayed conditionally when the listing component has an id, indicating that it already exists within the database and its standing could be edited. Moreover, we apply conditional courses to the <.icon> primarily based on the merchandise’s standing, to vary its colour.

Now, let’s add a very powerful half, the textual content enter to to edit every listing merchandise:

This is a screenshot of the shopping list component. In the screenshot, there is a text input field highlighted. This text input field is used to create new items or update existing ones in the shopping list.

We add two textual content inputs to ship the parameters of our merchandise: the :identify and the :list_id to which it belongs:

def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={type <- @gadgets} id={"listing#{@list_id}-item#{merchandise.id}"}>
          <.simple_form ...>
            <div class="flex">
              ...
+             <div class="flex-auto block text-sm leading-6 text-zinc-900">
+               <enter kind="hidden" identify={type[:list_id].identify} worth={type[:list_id].worth} />
+               <.enter
+                 area={type[:name]}
+                 kind="textual content"
+                 phx-target={@myself}
+                 phx-key="escape"
+                 phx-keydown={
+                   !type.information.id &&
+                     JS.push("discard", goal: @myself, worth: %{list_id: @list_id})
+                 }
+                 phx-blur={type.information.id && JS.dispatch("submit", to: "##{type.id}")}
+               />
+             </div>
            </div>
          </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
finish

Let’s take a more in-depth take a look at the weather we added. First, we embrace a hidden enter area to retailer the id of the listing to which the component belongs. This permits us to ship it as a part of the parameters for the validate and save occasions by setting the worth={type[:list_id].worth} attribute.

Subsequent, we introduce a barely extra advanced <.enter> part with occasion choices. By utilizing the phx-key and phx-keydown attributes, we specify that urgent the escape key triggers the discard occasion despatched to the server.

We’ve got two methods to avoid wasting adjustments: urgent Enter to set off the submit occasion or permitting adjustments to be mechanically saved when the enter loses focus.

For components that exist already within the database and are modified, the phx-blur binding comes into play. It mechanically submits the shape when the enter loses focus, guaranteeing that adjustments are saved seamlessly.

Lastly, we add a button with an icon to delete current components from the database:

This is a screenshot of the shopping list component. In the screenshot, there is a button highlighted in a red square. This button is used to delete the corresponding item from the shopping list.

def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={type <- @gadgets} id={"listing#{@list_id}-item#{merchandise.id}"}>
          <.simple_form ...>
            <div class="flex">
              ...
+             <button
+               :if={type.information.id}
+               kind="button"
+               class="w-10 -mt-1 flex-none"
+               phx-click={
+                 JS.push("delete", goal: @myself, worth: %{id: type.information.id})
+                 |> disguise("#listing#{@list_id}-item#{type.information.id}")
+               }
+             >
+               <.icon identify="hero-x-mark" />
+             </button>
            </div>
          </.simple_form>
        </div>
      </div>
    </div>
  </div>
  """
finish

We set off the delete occasion whereas concurrently hiding the type of the merchandise we want to take away.

We now add the ultimate part to our part. We embrace two buttons: one so as to add a brand new merchandise to the listing, and one other to delete all gadgets from the listing.

This is a screenshot of the shopping list component. Below the list items, there is a highlighted section containing two buttons. The first button is used to add a new item form to the list, while the second button is used to reset the list items.

def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      <.header>...</.header>
      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
        <div :for={type <- @gadgets} id={"listing#{@list_id}-item#{merchandise.id}"}>
          ...
        </div>
      </div>
+     <.button phx-click={JS.push("new", goal: @myself, worth: %{list_id: @list_id})} class="mt-4">
+       Add merchandise
+     </.button>
+     <.button
+       phx-click={JS.push("reset", goal: @myself, worth: %{list_id: @list_id})}
+       class="mt-4"
+     >
+       Reset
+     </.button>
    </div>
  </div>
  """
finish

We despatched occasions to the server utilizing JS.push. Each the reset and new occasions obtain the list_id as a parameter.

Superior! We have completed the markup for our part!

We are able to simply render it by passing an inventory struct just like the one we noticed above:

def render(assigns) do
  ~H"""
  <div id="lists" class="grid sm:grid-cols-1 md:grid-cols-3 hole-2">
    <.live_component
      :for={listing <- @lists}
      module={ComponentsExamplesWeb.ListComponent}
      id={listing.id}
      listing={listing}
    />
  </div>
  """
finish

However earlier than we deal with dealing with all of the occasions we outlined, how about we optimize reminiscence utilization through the use of Streams? With just some tweaks, we will implement this characteristic and guarantee we’re not storing all of the listing components in reminiscence.

Converting to Streams

To optimize memory usage, let’s start by making a small change to our assigns. We assign the stream items using the stream/4 perform:

def replace(%{listing: listing} = assigns, socket) do
  item_forms = Enum.map(listing.gadgets, &build_item_form(&1, %{list_id: assigns.id}))

  socket =
    socket
    |> assign(
      list_id: listing.id,
      list_name: listing.title,
-     gadgets: item_forms
    )
+   |> stream(:gadgets, item_forms)

  {:okay, socket}
finish

Subsequent, we outline the required phx-update DOM attribute on the mum or dad container the place the merchandise assortment is rendered. The gadgets at the moment are accessed by way of the brand new assign @streams, and we eat the stream utilizing a comprehension:

def render(assigns) do
  ~H"""
  <div class="bg-gray-100 py-4 rounded-lg">
    <div class="space-y-5 mx-auto max-w-7xl px-4 space-y-4">
      ...
-      <div id={"#{@list_id}-items"} class="grid grid-cols-1 gap-2">
+      <div
+        id={"#{@list_id}-items"}
+        class="grid grid-cols-1 gap-2"
+        phx-update="stream"
+      >
-        <div :for={type <- @gadgets} id={"listing#{@list_id}-item#{merchandise.id}"}>
+        <div :for={{id, type} <- @streams.gadgets} id={id}>         
        ...
        </div>
      </div>
      ...
    </div>
  </div>
  """
finish

Now we’re all set to deal with the occasions we outlined earlier!

Fly.io is an effective way to run your Phoenix LiveView app near your customers. It is very easy to get began. You could be operating in minutes.

Deploy a Phoenix app today!  

Add, Delete, Update, Reset Streams

We’ll be defining five events (new, validate, save, delete, reset). Our essential focus is clarify how we will use streams to replicate adjustments in our listing. We can’t be diving into features that work together with the database.

To get began, it is going to be useful to outline a helper perform that creates types already related to the list_id:

defp build_empty_form(list_id) do
  build_item_form(%Merchandise{list_id: list_id}, %{})
finish

Take into accout this perform, in addition to the one we outlined earlier. We’ll be utilizing 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 perform:

def handle_event("new", %{"list_id" => list_id}, socket) do
  {:noreply, stream_insert(socket, :gadgets, build_empty_form(list_id), at: -1)}
finish

To insert the brand new type on the finish of the listing, we use the :at choice offered by the stream_insert/4 perform.

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.

See Also

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 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 additionally receives changesets as a parameter:

def handle_event("delete", %{"id" => item_id}, socket) do
  merchandise = SortableList.get_item!(item_id)
  {:okay, _} = SortableList.delete_item(merchandise)
  {:noreply, stream_delete(socket, :gadgets, build_item_form(merchandise, %{}))}
finish

You can too use the stream_delete/3 perform to discard the type of the brand new component when wanted:

def handle_event("discard", params, socket) do
  {:noreply, stream_delete(socket, :gadgets, build_empty_form(params["list_id"]))}
finish

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.

Hooray! We have finished it! Our part is now full and able 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 options in motion. We used our previous learnings to create an much more spectacular part!

Credits

A big shoutout to Chris McCord for sharing the incredible example that impressed these posts and for patiently answering any questions in regards to the thrilling new ideas in Phoenix and LiveView.



Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top