dakora

dakora

Hello all!

I’ve been working a few days on trying to solve a relatively simple issue, however it’s turning into quite the road block. I have a Live Component. which is nested inside of a Form Component which I’ve used to replace a custom AJAX search box which was dreadful to use and consisted of 400 lines of javascript.

I’m having an issue attempting to reconstruct part of the functionality, which basically involves copying the value in short to a related sibling search box if it happens to be empty.

I’ve had little luck so far in getting the two Live Components to talk to each other without causing a mess.

The first idea i had was to use Phoenix PubSub, but discovered live components can’t have handle info, so that goes to the parent, which is fine. I decided to try the more direct route, and use send/2 which gets my message to the parent however it looks like the only option is send_update/2 to get anything to a child liveview.

That won’t work because calling update wipes out all of the existing assigns for the component, and there is no way to get the originals back to it (as its a different component thats requesting the update). Back to square one!

I’m not terribly familiar with send/2, however that could be another avenue to explore. I don’t really see anyway to let the requesting component know which process it needs to send to. I tried backtracking through the Live Component code for send_update, but it does a weird thing with the Phoenix Channel and it doesn’t look obvious how it actually triggers update on the correct component by ID.

What are the other options for communicating between Live Components? The less desirable I can think of would be to fire up full blown Live Views for every search box, but that seems too heavy for one input and some basic search functionality.

Showing Posts 1 to 10

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

This shouldn’t be true, are you using stateless or stateful components? To do the kind of communication you want you definitely need stateful live components, and in those cases update does not clobber existing assigns.

sfusato

sfusato

I usually have a simple public API for each component that needs to be talked to. Something like:

defmodule LiveComponentTwo do
  ######### Public API
  def update_text(id, text) do
    send_update(__MODULE__, id: id, update: :update_text, text: text)
  end
  #########

  def update(%{update: :update_text, text: text}, socket) do
    # I suppose here you could check if the value in the socket is empty
    {:ok, assign(socket, :text, text)}
  end

  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end
end

then, LiveComponentTwo.update_text("id_of_the_component", "text")

Notice that I add a key update: :update_text and then pattern match on that key in the update callback.

dakora

dakora OP

If i’m understanding the differentiation in the docs I believe they are stateful. I use handle_event all over them and they are Phoenix.LiveComponent’s and not Phoenix.Component’s. My update function looks like this:

def update(assigns, socket) do
    name = String.to_existing_atom("#{Map.get(assigns, :assoc)}_id")

    socket = socket
    |> assign(assigns)
    |> assign([items: [], vid: nil, index: 0, count: 0, name: name])

    {:ok, assign(socket, value: get_value(socket.assigns))}
  end

When I call send_update from the parent i’m only passing vid and value (as I don’t have access to the other assigns needed at this point), and inspecting update those are the only variables available in assigns.

send_update(Search, id: id, value: value, vid: vid)

> Inferred IO.inspect In the Update Function
> %{                                                                                                                                                                                                                                                                            
  id: "component id",                                                                                                                                                                                                                                                
  value: "field value",                                                                                                                                                                                                                                                   
  vid: "field id"                                                                                                                                                                                                                                                
} 
dakora

dakora OP

I did try pattern matching like that however I ran into the same issue where it wiped out all of my assigns.

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Can you show an example? I have lots of code that works this way and it works just fine.

dakora

dakora OP

Here’s the Live Component in question. Note that it’s a prototype and employs thousands of dirty, no good hacky hacks.

defmodule Proj.Search do
  use Phoenix.LiveComponent
  use Phoenix.HTML

  import Ecto.Query

  alias Proj.Repo

  @impl true
  def update(assigns, socket) do
    name = String.to_existing_atom("#{Map.get(assigns, :assoc)}_id")

    socket = socket
    |> assign(assigns)
    |> assign([items: [], vid: nil, index: 0, count: 0, name: name])

    {:ok, assign(socket, value: get_value(socket.assigns))}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <span>
      <input
        id={@id}
        class={iclass(assigns)}
        autocomplete="off"
        phx-target={@myself}
        phx-keyup="input"
        phx-hook="SearchInput"
        autofocus={Map.get(assigns, :autofocus)}
        tabindex={Map.get(assigns, :tabindex)}
        value={@value}/>
      <div class="sc-container nueue"><%= for {item, i} <- Enum.with_index(@items) do %>
        <div class={class(i, @index)}>
          <div class="sc-tit" phx-click="fin" phx-value-id={item.id} phx-target={@myself}><%= Map.get(item, @search) %></div>
        </div>
      <% end %></div>
      <%= if not is_nil(@value) do %>
        <%= hidden_input @f, @name, value: @vid %>
      <% end %>
    </span>
    """
  end

  @impl true
  def handle_event("input", %{"key" => "ArrowUp"}, socket) do
    ind = socket.assigns.index
    index = if ind == 0, do: socket.assigns.count - 1, else: ind - 1
    s = Enum.at(socket.assigns.items, index)
    {:noreply, assign(socket, index: index, value: Map.get(s || %{}, socket.assigns.name), vid: Map.get(s || %{}, :id))}
  end

  def handle_event("input", %{"key" => "ArrowDown"}, socket) do
    ind = socket.assigns.index
    index = if ind == socket.assigns.count - 1, do: 0, else: ind + 1
    s = Enum.at(socket.assigns.items, index)
    {:noreply, assign(socket, index: index, value: Map.get(s || %{}, socket.assigns.name), vid: Map.get(s || %{}, :id))}
  end

  def handle_event("input", %{"Key" => k, "value" => val}, socket) when k in ["Tab", "Enter"] do
    if val == socket.assigns.value do
      {:noreply, socket}
    else
      handle_event("fin", %{"id" => socket.assigns.vid}, socket)
    end
  end

  def handle_event("input", %{"value" => val}, socket) do
    assigns = socket.assigns
    if assigns.value == val do
      {:noreply, socket}
    else
      q = val <> "%"
      i = if val == "" do
        []
      else
        source = Ecto.build_assoc(assigns.f.data, assigns.assoc).__struct__
        from(s in source)
        |> where([s], field(s, :active) == ^true and ilike(field(s, ^assigns.search), ^q))
        |> order_by([s], [asc: field(s, ^assigns.search)])
        |> limit(20)
        |> Repo.all()
      end

      vid = if Enum.empty?(i) do nil else Map.get(List.first(i), :id) end

      {:noreply, assign(socket, items: i, count: Enum.count(i), vid: vid, index: 0)}
    end
  end

  def handle_event("fin", %{"id" => id}, socket) do
    s = Enum.find(socket.assigns.items, fn x -> x.id == id end)
    value = Map.get(s || %{}, socket.assigns.search)
    if Map.get(socket.assigns, :affect) do
      send(self(), {:affect, id: socket.assigns.affect, value: value, vid: id})
    end
    {:noreply, assign(socket, items: [], count: 0, value: value, vid: id, index: 0)}
  end

  defp class(i, ind) do
    if i == ind do
      ["sc-t", "active"]
    else
      "sc-t"
    end
  end

  defp iclass(assigns) do
    [Map.get(assigns, :class, [])] ++ ["search"]
  end

  defp get_value(assigns) do
    (Proj.get_assoc(assigns.f, assigns.name, assigns.assoc) || %{}) |> Map.get(assigns.search)
  end
end

And the function call in the parent:

def handle_info({:affect, id: id, value: value, vid: vid}, socket) do
    send_update(Proj.Search, id: id, value: value, vid: vid)
    {:noreply, socket}
end
benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Right I guess what I was trying to look for was a version of the pattern matching on update that wasn’t working as you expected. The version you’ve posted here has a single def update clause.

dakora

dakora OP

Well now that i’ve gone back and combed over the multiple update defs method it’s working for me! It sure would be nice if this behaviour was documented…

Thank you for pointing me in the right direction

annad

annad

Thank you SO much for posting this example! I just ran into the same issue when trying to communicate between components and this is incredibly helpful. I had not thought of writing two update functions and then pattern matching on a specific update. Quick question …

If I’m understanding your approach correctly, you provide a public API that then calls send_update on itself. Why don’t you have LiveComponent1 just call send_update directly onto LiveComponent2 using that update: key?

sfusato

sfusato

It’s cleaner this way, easier to refactor, it’s obvious that this component is called from outside just by opening the file.

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New

Other Trending Topics Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews