Phoenix liveView is sending all the articles in diff even though there is not any actual change

I need help, so I have live page inside it a for loop that iterate an article component, the article component have phx_click that toggle a form to insert some data for this particular article.
I discovered that clicking on the article there a delay when I looked into the websocket I found that inbound connection have all the articles in diff.

this my render function in the article component

  def render(assigns) do
    ~L"""
    <p class="article-text" style="margin-bottom: 10px"  id="<%= @article.id %>">
          <%= for word <- body_to_words(@article.content) do %>
            <span phx-click="word-clicked" phx-update="replace" phx-target="<%= @myself %>" phx-value-law="<%= @article.law_id %>" phx-value-article="<%= @article.id %>" phx-value-offset="<%= word.offset %>" >
            <%= word.text %>
            <%= if  @show_toolbar &&  @article.id == elem(Integer.parse(@clicked_word.article), 0) && @clicked_word.offset == word.offset do%>
            <span  class="reference is-active">()</span>
            <% end %>
            </span>
            <% references = Enum.filter(@article.references, fn ref -> ref.offset == word.offset end) %>
            <%= if length(references) > 0  do %>
              <%= if length(references) > 1  do %>
            <span class="reference">
              <%= for ref <- references do %>
                <%= ref.text %>
              <% end %>
              </span>
            <% else %>
              <%= for ref <- references do %>
                <span class="reference"><%= ref.text %> </span>
              <% end %>
            <% end %>
            <% end %>
          <% end %>
        </p>
    """
  end

and here part of the editor.

 <div class="card">
      <div class="card__body">
        <div id="laws-<%= @counter %>" phx-update="append"  class="stylesheet searchable-context">
        <%= for article <- @articles do %>
          <%= live_component @socket, ArticleComponent, article: article, show_toolbar: @show_toolbar, reference_changeset: @reference_changeset, id: article.id, clicked_word: @clicked_word %>
          <% end %>
        </div>
        <div id="footer" phx-hook="InfiniteScroll">Load More</div>
      </div>
    </div>

here image of what I see in the websocket
Imgur

LiveView won’t be able to diff-track what’s inside ArticleComponent due to this:
<%= for word <- body_to_words(@article.content) do %>

I see phx-update="append". Have you declared @articles as a temporary assign? How do you insert/update data for each particular article inside @articles? This is where the key is.

1 Like

@sfusato I’m a certified grade A idiot, I didn’t add @articles as temporary_assign.
I could have sworn I added it but it wasn’t there, now checking the sockets I only get the article I clicked on.

@sfusato Sorry I need your help again

so I have the article component

 def update(assigns, socket) do
    IO.inspect(assigns.clicked_word)
    socket = assign(socket, assigns)
    {:ok, assign(socket, :words, body_to_words(socket.assigns.article.content))}
  end

  @impl true
  def render(assigns) do
    ~L"""
    <p class="article-text" style="margin-bottom: 10px"  id="<%= @article.id %>">
          <%= for word <- @words do %>
            <span phx-click="word-clicked" phx-target="<%= @myself %>" phx-value-law="<%= @article.law_id %>" phx-value-article="<%= @article.id %>" phx-value-offset="<%= word.offset %>" >
            <%= word.text %>
            <%= if  @show_toolbar &&  @article.id == @clicked_word.article && @clicked_word.offset == word.offset do%>
            <span  class="reference is-active">()</span>
            <% end %>
            </span>
            <%= if length(filter_references(@article.references, word)) > 0  do %>
              <%= if length(filter_references(@article.references, word)) > 1  do %>
            <span class="reference">
              <%= for ref <- filter_references(@article.references, word) do %>
                <%= ref.text %>
              <% end %>
              </span>
            <% else %>
              <%= for ref <- filter_references(@article.references, word) do %>
                <span class="reference"><%= ref.text %> </span>
              <% end %>
            <% end %>
            <% end %>
          <% end %>
        </p>
    """
  end

  def filter_references(references, word) do
    Enum.filter(references, fn ref -> ref.offset == word.offset end)
  end

  def body_to_words(body) do
    words =
      String.split(body, " ")
      |> Enum.map(fn w -> %{text: String.trim(to_string(w))} end)

    {_final_offset, with_offset} =
      Enum.reduce(words, {0, []}, fn word, {offset, words} ->
        length = String.length(word.text)
        end_offset = offset + length
        word_with_offset = Map.put(word, :offset, end_offset)
        {end_offset, words ++ [word_with_offset]}
      end)

    with_offset
  end

  @impl true
  def handle_event("word-clicked", %{"offset" => text_offset, "article" =>article_id, "law" => law_id}, socket) do
    {offset, _remainder} = Integer.parse(text_offset)
    send(self(), {:word_clicked, %{offset: offset, article: elem(Integer.parse(article_id), 0), law: law_id}, true})
    {:noreply, socket}
  end
end

now send(self(), {:word_clicked, %{offset: offset, article: elem(Integer.parse(article_id), 0), law: law_id}, true}) {:noreply, socket} send to the parent this map and in the parent I update @clicked_word assign but for some reason the component doesn’t re-render.

I assume it doesn’t re-render automatically since @articles hasn’t changed (being a temporary assign, it’s empty bassically), thus the for isn’t re-run.

You want to update only a specific ArticleComponent or all of them? I assume all, otherwise, you could have handled it in the stateful component. I think the options are either to re-assign all of the articles, or use send_update to update all components with the new @clicked_word assign. You’ll need their id’s to loop over them.

1 Like

so I tried send_update

>   def handle_info({:word_clicked, clicked_word, show_toolbar}, socket) do
>     send_update(ArticleComponent, id: clicked_word.article, clicked_word: clicked_word)
>     {:noreply, assign(socket, clicked_word: clicked_word, show_toolbar: show_toolbar)}
>   end

I send the correct ID but the component doesn’t re-render.

EDIT: I understand now, @articles are temporary assign so I need to refetch the one I want to change.

now it is all working after I changed my code:

def handle_info({:word_clicked, clicked_word, show_toolbar}, socket) do

    send_update(ArticleComponent, id: socket.assigns.clicked_word.article, clicked_word: clicked_word)

    {:noreply, assign(socket,clicked_word: clicked_word, show_toolbar: show_toolbar, articles: [Laws.get_article!(clicked_word.article)])}

  end

The idea of my code was when the user clicks on word a bracket “()” appears where he going to insert the reference and “()” should only appear once and not multiple times across different articles.

so I use send_update to clear the old “()” from article id ex:12 and in assign I fetch the article where I want “()” to appear for example id: 13