William
William

Reputation: 4578

Elixir Phoenix Liveview Sorting Minefield Problem Navigating Render, Socket, Params

The image below is an app that I am working on and attempting to add a feature to.

https://imgur.com/a/f1dEP0E

Quick description of problem.

In the video below when I click the Hardware heading each item sorts alphabetically. This is good and what I want. When I click the Available or Taken buttons to update the status, the items immediately re-order based on updated_at property and this is not what I want. My goal is to write code that ensures that no sorting changes happen when the user changes the Available/Taken status.

https://imgur.com/a/ZOXx86J

Quick description of initial code

The items that are stacked on top of one another are called "testbeds" and these are pulled from Postgres via Ecto. They are then assigned to the socket, rendered and iterated through. TestBeds uses a pub-sub feature so when changes are made to this collection the changes are broadcast to other users.

 def mount(_params, _session, socket) do
    TestBeds.subscribe()
           
    {:ok,
     assign(socket,
       testbeds: TestBeds.list_testbeds(),    #Get Testbeds
       # code ...
     )}
  end

Render

def render(assigns) do
   ~H"""
   <body>
   
   <div>
   
   <%= for testbed <- Enum.filter(@testbeds, fn(item)-> item.group_id != nil end)|>Enum.sort_by(&("# {&1.group.name}#{&1.name}"), :asc) do %>
       
       <%!-- code.... --%>
     
   <% end %>
   </div>

   
   <%!-- code ..... --%>
   
   </body>
   """
end

In the code above the testbeds are explicitly sorted by a property named group.name. The reason I am sorting it like this is because if I iterate and render testbeds with no sorting, the testbeds will automatically sort by the update_at property (and this change will be broadcast to other users) and this is not what I want. Choosing to sort it by group.name is arbritary, I could have used any property.

What is the problem?

You see those headers at the top? I want the user to be able to click each one and as a result the testbeds automatically sort by that columns data.

So far I have it partially working. I updated the testbeds with the previous sorting removed and so the code now looks like this:

   <%= for testbed <- @testbeds do %>
       
       <%!-- code.... --%>

      <%-- Example of sorting applied to a heading. The actual code has about 12 of these --%>
      <h2 class="info-button" phx-click="sort_by_string" phx-value-field="hardware"> Hardware</h2>   

      <%!-- code.... --%>

     
   <% end %>

The code to sort the testbeds looks like this:

  def handle_event("sort_by_string", %{"field" => field}, socket) do

     atomParam = String.to_existing_atom(field)

     IO.inspect atomParam

    if socket.assigns[:sort_by_name_ascending] == false do
      sorted_testbeds = Enum.sort_by(socket.assigns.testbeds, fn item -> Map.get(item, atomParam) end)   #hardware is hard coded as an atom
      {:noreply, assign(socket, testbeds: sorted_testbeds, sort_by_name_ascending: true)}
    else
      sorted_testbeds = Enum.sort_by(socket.assigns.testbeds, fn item -> Map.get(item, atomParam) end) |> Enum.reverse()  #hardware is hard coded as an atom
        dbg(sorted_testbeds)
      {:noreply, assign(socket, testbeds: sorted_testbeds, sort_by_name_ascending: false)}
    end
  end

The Problem

The field titled Set Status (see image) has a button that lets the user make an update to a property of testbeds named status (in the image this is the button displaying Available or Taken). When this happens, the testbed order changes and re-orders based on the values in update_at property. I do not want this. I want the order of the testbeds to stay as-is and not to be broadcast to users. However, I do want the status update (the change from Available to Taken) to be broadcast. To be clear, when a user changes the status no sorting should be visibly made or broadcast but the status value change should be broadcast. The status values are Available and Taken (see image).

To make this worse, as mentioned, I have integrated the pub-sub feature so that when a user changes the status of a testbed, not only does the status change but the ordering is broadcast to everyone. I want the ordering to visibly stay as-is for any user that changes a testbed status.

The status code is here:

  def handle_event("create-status", params, socket) do
    # Create  Record

    %{"status" => status, "testbed_id" => testbed_id, "developer" => developer} = params

    stripped_developer = String.trim(developer)


     if stripped_developer === "" || String.trim(developer) === "" || String.trim(developer) === "None" do
      {:noreply, assign(socket, developer: developer,  name_warning: " (can't be empty or set to None)")}

     else
      TestBeds.get_test_bed!(testbed_id)
      |> TestBeds.update_test_bed(%{status: status, developer: developer})

      current_testbed = TestBeds.get_test_bed!(testbed_id)

      StatusActions.create_status_action(%{
        testbed_name: current_testbed.name,
        testbed_value: testbed_id,
        status: status,
        developer: developer
      })

   
      {:noreply, assign(socket, developer: developer)}
    end
  end
The reset (Changes **Taken** back to **Available**) is here:


  def handle_event("reset", %{"id" => id}, socket) do
    testbed = TestBeds.get_test_bed!(id)

    StatusActions.create_status_action(%{
      testbed_name: testbed.name,
      testbed_value: id,
      status: "Available",
      developer: testbed.developer
    })


    TestBeds.get_test_bed!(id)
    |> TestBeds.update_test_bed(%{status: "Available", developer: "None"})


     # {:noreply, socket}
    {:noreply, assign(socket, testbeds: TestBeds.list_testbeds())}
  end

Summary

When a user sorts data by clicking the headings. the testbeds should sort by column data and this sorting should not be broadcasted to other users. This currently works with one exception.

The exception When a user changes the Status value sorting takes and is broadcast to other users. This should not happen.

Upvotes: 0

Views: 26

Answers (1)

Cubic
Cubic

Reputation: 15673

So the issue starts with having the sorting in rendering. It doesn't belong there and you've already improved it by moving it out of there, so good on that!

There is a misunderstanding here as well. You write:

but the ordering is broadcast to everyone

Actually, no ordering is broadcast to anyone. The issue is rather the way you wrote it your view doesn't remember what you want to sort by, so when a new list comes in you just take it in whatever order it happens to be rather than the one you actually want.

Some remaining issues:

  1. Your sort-by option is somewhat convoluted right now. Why is there a sort_by_name_ascending field and why is it a boolean? This could just be a single field sort_by: {:name | ... | :hardware, :asc | :desc}.
  2. If a user picks a sort by option you're not remembering the chosen option anywhere. You're just flipping whether to order ascending or descending.
  3. Because you're not remembering your chosen sort option anywhere, you can not apply it when the list gets updated.

So let's fix these one by one.

  1. In mount, initially record we'd like to sort by name ascending:
assign(socket,
  ...,
  sort_by: {:name, :asc})

Add a helper function to sort our test beds by our chosen option:

defp sort_testbeds(%{assigns: %{testbeds: testbeds, sort_by: {field, order}} = socket) do
  # sort_by already takes an option to order asc/descending, no need for a Enum.reverse here
  assign(socket, testbeds: Enum.sort_by(testbeds, &Map.get(&1, field), order))
end

Update the handle event:

# I like to use ecto here, but of course you don't have to
@sort_by_enum Ecto.Parameterized.init(Ecto.Enum, values: [:name, :hardware])
def handle_event("sort_by_string", %{"field" => field}, socket) do
  {sort_by_field, order} = socket.assigns.sort_by
  {:ok, new_sort_by_field} = Ecto.Type.cast(@sort_by_enum, field)
  new_sort_by = if new_sort_by_field == sort_by_field do
    {sort_by_field, if(order == :asc, do: :desc, else: :asc)}
  else
    {sort_by_field, :asc}
  end
  socket |> assign(sort_by: new_sort_by) |> sort_testbeds()
end

And make sure we sort again when the new testbeds come in:

{:noreply, socket |> assign(testbeds: TestBeds.list_testbeds()) |> sort_testbeds()}

Now even more likely though is that you the sorting code shouldn't be in the View code at all; I don't know what your application is, maybe this Testbeds list will only ever contain a handful of items, but you certainly don't want to load, reload and sort a list of thousands, tens of thousands or millions of items whenever something happens. More likely you'll want to move your sorting closer to your data storage layer, so in the end you'll just call

Testbeds.list_testbeds(sort_by: socket.assigns.sort_by)

whenever you get notified of a change in testbeds or the order changes; And probably you'll want to add limits or a way to stream the results there too. But the right thing to do there really depends on the specifics of your application.


Another important point: This simple sort option will work for 'basic' fields like string, atom or integer fields. However, for things like Date the builtin sort-order doesn't work anymore, you'd need to tell Enum.sort_by to use the DateTime order: Enum.sort_by(items, & &1.updated_at, {:asc, DateTime}) for instance - otherwise you might find somewhat unintuitive results like

~U[2004-05-01 00:00:00Z] < ~U[1992-05-01 23:59:59Z]
# => true
Enum.sort([~U[2004-05-01 00:00:00Z], ~U[1992-05-01 23:59:59Z]])
# => [~U[2004-05-01 00:00:00Z], ~U[1992-05-01 23:59:59Z]]

When what you probably meant was

DateTime.before?(~U[2004-05-01 00:00:00Z], ~U[1992-05-01 23:59:59Z])
# => false
Enum.sort([~U[2004-05-01 00:00:00Z], ~U[1992-05-01 23:59:59Z]], DateTime)
# => [~U[1992-05-01 23:59:59Z], ~U[2004-05-01 00:00:00Z]]

The same thing applies for all other types that may need different sort orders than the default erlang term order. That is of course moot if you apply your sort order to your query instead, which like I mention above you're likely to do at some point anyway.

Upvotes: 1

Related Questions