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

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
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
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews