A Reusable Multi-Tag Selection Component for Phoenix LiveView

A Reusable Multi-Tag Selection Component for Phoenix LiveView

In this article I'm gonna show how to create a reusable multi-tag selection component for a Phoenix LiveView

ezgif.com-gif-maker (1).gif

Prerequisite

  • phoenix_live_view

  • tailwind css

Add a live route

  • go to router.ex and add
live "/" ParentLiveView

Create a Phoenix LiveView

  • in your live folder create a new file parent_live_view.ex
defmodule ParentLiveView do
  use Phoenix.LiveView

  @impl true
  def mount(_params, _session, socket) do
     socket = assigns(socket, :items, [])
     {:ok, socket}
  end
end
  • we created an assign items with a data type of array. This will help us store the tags later on.

Create the liveview template

  • in your live folder create a new file parent_live_view.html.heex

  • assuming that you already have a changeset together with its form and decided to have a multi tag select input that you need

  • add the live_component

...
<.live_component module={MultiTagSelect} form={f} field={:field} items={@items} placeholder="Press comma to after each item" />
...

Create the live_component

  • in your live folder, create a new folder components

  • inside live/components, add a new file multi_tag_select.ex

defmodule MultiTagSelect do
  use Phoenix.LiveComponent

  @impl true
   def render(assigns) do
      ~H"""
         <div class="flex flex-col border border-gray-300 rounded rounded-md p-3 bg-white">
      <div class="wrap-items">
      <%= for item <- @items do %>
        <div class="flex flex-row justify-center  align-center rounded rounded-md text-center bg-gray-300 text-blue-900 p-2 m-1">
        <%= item %>
        <%= Heroicons.icon("x", phx_click: "remove-item", phx_target: @myself, phx_value_item: item, type: "solid", class: "rounded rounded-full border border-red-600 fill-red-700 self-center h-4 w-4 ml-3 cursor-pointer") %>
        </div> 
        <% end %>
    <%= text_input @form, @field, placeholder: @placeholder, class: "flex-1 appearance-none px-3 py-3 border border-0 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue focus:border-blue focus:z-10 sm:text-sm ", phx_target: @myself, phx_hook: "CreateItem" %>
      </div> 
    </div> 
       """
   end

   @impl true
   def mount(socket) do
    {:ok, socket}
   end

end

Lets Disect

phx-target and phx-hook on text_input

...
  <%= text_input @form, @field, placeholder: @placeholder, class: "flex-1 appearance-none px-3 py-3 border border-0 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue focus:border-blue focus:z-10 sm:text-sm ", phx_target: @myself, phx_hook: "CreateItem" %>
...
  • phx_target: @myself will target the live_component instead of the liveview

  • phx-hook: <name-of-hook> on the other hand help us to hook a JS function on the client side.

Implement the Hook

  • go to your app.js and apply this code
let Hooks = {};

Hooks.CreateItem = {
  mounted() {
    this.el.addEventListener("keyup", (e) => {
      if (e.code == "Comma") {
        let tags = e.target.value.replace(/\s+/g, "");
        tags.split(",").forEach((tag) => {
          if (tag.length > 0) {
            this.pushEvent("add-item", tag);
          }
        });
        e.target.value = "";
      }
    });
  },
};

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: Hooks,
  ...
});
  • remember phx_hook: "CreateItem"? This is where we will implement the hook and wait for the changes and use it for our multi-tag-select live-component feature.

  • we add an addEventListener with a keyup capability. We wait for the user to put any value and when an event reaches Comma that's when the value will be inserted via a pushEvent.

  • this.pushEvent("add-item", tag) on the other hand will push an event to our server and we should handle it with handle_event/3 in our parent live_view in order for us to update the state of our liveview.

Going back to our parent liveview

  • apply this code changes
...
 @impl true
  def handle_event("add-item", item, socket) do
    items = [item | socket.assigns.items]
    socket = assign(socket, :items, items)
    {:noreply, socket}
  end
...
  • at this rate, every time the user types in different characters followed up by a comma, it will trigger an event. At this case it is the add-item event.

  • at this case, you can now update the state of our items in our parent liveview.

Remove an Item

Now that we are able to add an item let's discuss how the delete mechanism works. Going back to the live_component

phx-target and phx-click and phx-value-

...
 <%= Heroicons.icon("x", phx_click: "remove-item", phx_target: @myself, phx_value_item: item, type: "solid", class: "rounded rounded-full border border-red-600 fill-red-700 self-center h-4 w-4 ml-3 cursor-pointer") %>
...
  • phx-click: "remove-item" helps us to monitor any click action coming from this component. Two things we need to do here.

  • phx-target: @myself will help us target the component instead of the liveview itself.

  • phx-value-<value-name> help us to assign a value that will later on help us to remove an item

Now

  • Add a handle_event/2 function inside our component. This will listen to every click we make in our x icon.
 @impl true
  def handle_event("remove-item", %{"item" => item} = _params, socket) do
    send(self(), {:remove_item, item})
    {:noreply, socket}
  end
  • send/2 is a part of the code wherein every time an x icon was click, we are sending info to our liveview that we want to delete this item. Now we need to add a handle_info/2 function inside our liveview to listen for that event.

  • inside our parent liveview

 @impl true
  def handle_info({:remove_item, item}, socket) do
    items = Enum.filter(socket.assigns.items, &(&1 != item))
    socket = assign(socket, :items, items)
    {:noreply, socket}
  end
  • at this rate, every time the user clicks the x icon, live_component will listen to the click event and will send info to our parent live view that the item click will be deleted.

Happy Coding!

ย