In this article I'm gonna show how to create a reusable multi-tag selection component for a Phoenix LiveView
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 filemulti_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 liveviewphx-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 akeyup
capability. We wait for the user to put any value and when an event reachesComma
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 withhandle_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 ourx
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 ahandle_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!