Need advise to update nested list 3-level

Hello friends, I have a list that I want to update it (3 level nested), and I need your suggestion to create better code

The list

elements = [
  %{
    children: [
      %{
        children: [
          %{
            children: [],
            class: ["text-black", "w-full", "p-2"],
            id: "c9ea0fff-1ee0-407e-8b2a-64afc954c40b",
            index: 0,
            parent: "section",
            parent_id: "62197198-e3a1-46de-8f18-f2c6843f646f",
            type: "text"
          },
          %{
            children: [],
            class: ["text-black", "w-full", "p-2"],
            id: "b2a6b171-26e1-4e8a-8cd3-c41f388de6e8",
            index: 1,
            parent: "section",
            parent_id: "62197198-e3a1-46de-8f18-f2c6843f646f",
            type: "text"
          }
        ],
        class: ["flex", "flex-col", "justify-between", "items-stretch",
         "min-h-[200px]", "w-full", "border", "border-dashed",
         "border-gray-400", "p-1"],
        id: "62197198-e3a1-46de-8f18-f2c6843f646f",
        index: 0,
        parent: "layout",
        parent_id: "64a2c3f2-d71f-464a-8318-be072d7b6624",
        type: "section"
      }
    ],
    class: ["flex", "flex-row", "justify-start", "items-center", "w-full",
     "space-x-3", "px-3", "py-10"],
    id: "64a2c3f2-d71f-464a-8318-be072d7b6624",
    index: 0,
    parent: "dragLocation",
    parent_id: "dragLocation",
    type: "layout"
  }
]

for updating and adding tag to the text element I did like this, but it is very dirty I think:

  def add_tag(elements, id, parent_id, layout_id, tag, type) when type in @elements do
    Enum.map(elements, fn
      %{type: "layout", id: ^layout_id, children: children} = selected_layout ->
        edited_list =
          children
          |> Enum.map(fn
            %{type: "section", id: ^parent_id, children: children} = selected_section ->
              element_edited_list =
                Enum.map(children, fn
                  %{type: ^type, id: ^id} = selected_element ->
                    Map.merge(selected_element, %{tag: tag})

                  element ->
                    element
                end)

              %{selected_section | children: element_edited_list}

            section ->
              section
          end)

        %{selected_layout | children: edited_list}

      layout ->
        layout
    end)
  end

MishkaTemplateCreatorWeb.MishkaCoreComponent.add_tag(elements, "c9ea0fff-1ee0-407e-8b2a-64afc954c40b", "62197198-e3a1-46de-8f18-f2c6843f646f", "64a2c3f2-d71f-464a-8318-be072d7b6624", "test1", "text")

By the way, for finding I created this code:

  def find_element(elements, id, parent_id, layout_id, type) when type in @elements do
    Enum.flat_map(elements, fn
      %{type: "layout", id: ^layout_id, children: children} ->
        case Enum.find(children, &(&1.id == parent_id)) do
          nil ->
            []

          %{type: "section", id: ^parent_id, children: children} ->
            if is_nil(data = Enum.find(children, &(&1.id == id))), do: [], else: [data]
        end

      _layout ->
        []
    end)
    |> List.first()
  end

Thank you in advance

Have you looked into Access.at/1 ?

4 Likes

Not sure if this is what you’re looking for Floki - HTML Parser

This one makes it easier to handle HTML parsing.

Also, I wouldn’t prefer to nest too much Enum.map as, it makes code reading a bit trickier, normally I would use recursion for clean code if it is more than 2 nesting to make it easier for maintenance…

2 Likes

I’d try to find a better data-structure first.
Flattening is a good idea oftentimes.
Why is data and CSS mixed?
What are you trying to accomplish?

1 Like

The combination of the Access module with the Kernel functions update_in/3 and put_in/3 is very useful for updating nested data structures.

1 Like

It looks like you might be doing a lot of data access.

You might get a huge boost in simplicity and performance by using maps instead of lists.
If you were willing to separate storing and sorting of children, you could store a map of children and an order list that has ordered child ids:

elements = %{
  children: %{
    "62197198-e3a1-46de-8f18-f2c6843f646f" => %{
      children: %{
        "b2a6b171-26e1-4e8a-8cd3-c41f388de6e8" => %{
          children: [],
          class: ["text-black", "w-full", "p-2"],
          id: "b2a6b171-26e1-4e8a-8cd3-c41f388de6e8",
          # index: 1,
          parent: "section",
          parent_id: "62197198-e3a1-46de-8f18-f2c6843f646f",
          type: "text"
        },
        "c9ea0fff-1ee0-407e-8b2a-64afc954c40b" => %{
          children: [],
          class: ["text-black", "w-full", "p-2"],
          id: "c9ea0fff-1ee0-407e-8b2a-64afc954c40b",
          # index: 0,
          parent: "section",
          parent_id: "62197198-e3a1-46de-8f18-f2c6843f646f",
          type: "text"
        }
      },
      class: ["flex", "flex-col", "justify-between", "items-stretch",
       "min-h-[200px]", "w-full", "border", "border-dashed", "border-gray-400",
       "p-1"],
      id: "62197198-e3a1-46de-8f18-f2c6843f646f",
      # index: 0,
      order: ["c9ea0fff-1ee0-407e-8b2a-64afc954c40b",
       "b2a6b171-26e1-4e8a-8cd3-c41f388de6e8"],
      parent: "layout",
      parent_id: "64a2c3f2-d71f-464a-8318-be072d7b6624",
      type: "section"
    }
  },
  order: ["62197198-e3a1-46de-8f18-f2c6843f646f"],
  class: ["flex", "flex-row", "justify-start", "items-center", "w-full",
   "space-x-3", "px-3", "py-10"],
  id: "64a2c3f2-d71f-464a-8318-be072d7b6624",
  # index: 0,
  order: "62197198-e3a1-46de-8f18-f2c6843f646f",
  parent: "dragLocation",
  parent_id: "dragLocation",
  type: "layout"
}

Once you do that, you can use @codeanpeace’s Access module functions at a cost of O(n) instead of O(n^3):

Look up would be:

def find(elements, id, parent_id, layout_id) do
  get_in(elements, [:children, layout_id,:children, parent_id, :children, id])
end

Updating an element

def add_tag(elements, id, parent_id, layout_id, tag, type) when type in @elements do
  update_in(elements, [:children, layout_id,:children, parent_id, :children, id], fn selected_element ->
      if selected_element.type == type do
        Map.merge(selected_element, %{tag: tag})
      end
  end)
end

Removing an element:

def delete(elements, id, parent_id, layout_id) do
  # Remove the child
  {_,elements} = pop_in(elements,  [:children, layout_id,:children, parent_id, :children, id])
  # Remove it from the order
  elements = update_in(elements,  [:children, layout_id,:children, parent_id, :order], fn order ->
    Enum.reject(order, &(&1 == id))
  end)
  elements
end

Adding is similar to deleting

3 Likes

@Sebb @pmangalakader

Repo: GitHub - mishka-group/mishka_template_creator: Mishka Template Creator for Phoenix and Phoenix LiveView
4 Likes

wow that looks really cool.

If I understand correctly you have an arbitrary nesting depth.
And you want to change an attribute in an arbitrary node, right?

So you can’t use a nested structure and a non-recursive algorithm to do that.

I would think about flattening the data in sth like %{node_id => node} where each node has an id and a ref to the parent’s id (see the test below for an example).

Here is some code I use to build a tree from a flat list:

defmodule Tools.TreeFromList do
  def build_tree(nodes, config) do
    by_parent = Enum.group_by(nodes, & &1[config.parent_id_key])
    Enum.map(by_parent[config.root_parent], &build_tree_(&1, by_parent, config))
  end

  defp build_tree_(node, nodes_by_parent, config) do
    children =
      Enum.map(
        Map.get(nodes_by_parent, node[config.node_id_key], []),
        &build_tree_(&1, nodes_by_parent, config)
      )

    config.build_tree_node.(node, children)
  end
end
defmodule TreeFromListTest do
  use ExUnit.Case

  @tag :build_tree
  test "tree from list" do
    data = [
      %{id: 1, name: "F1", parent_id: nil},
      %{id: 2, name: "F2", parent_id: nil},
      %{id: 6, name: "F6", parent_id: 3},
      %{id: 4, name: "F4", parent_id: 2},
      %{id: 5, name: "F5", parent_id: 3},
      %{id: 3, name: "F3", parent_id: 1}
    ]

    config = %{
      build_tree_node: &Map.put(&1, :children, &2),
      parent_id_key: :parent_id,
      node_id_key: :id,
      root_parent: nil
    }

    assert [
             %{
               children: [
                 %{
                   children: [
                     %{children: [], name: "F6", parent_id: 3, id: 6},
                     %{children: [], name: "F5", parent_id: 3, id: 5}
                   ],
                   name: "F3",
                   parent_id: 1,
                   id: 3
                 }
               ],
               name: "F1",
               parent_id: nil,
               id: 1
             },
             %{
               children: [%{children: [], name: "F4", parent_id: 2, id: 4}],
               name: "F2",
               parent_id: nil,
               id: 2
             }
           ] == Tools.TreeFromList.build_tree(data, config)
  end
end
2 Likes

I think I will have a big problem with changing the order of keys in children! when I use map Am I right?
Because my user can change the order of elements in a section!

I think the only way to change the sort or re-order is regenerate new UUID, it is works when I have just 2 elements is a section more than 2 and change by side 2 element, again it is a problem

For example:

<div id="1">
  <p>text-1</p>
  <p>text-2</p>
  <p>text-3</p>
</div>

// TO

<div id="1">
  <p>text-2</p>
  <p>text-1</p>
  <p>text-3</p>
</div>

Just use Pathex. It is universal solution for nested structures

If you can describe what you’re trying to achieve in a human language, I can write you a pathex lens

I see you want to put tag into type: "layout" ~> type: "section".

With pathex this would be something like

use Pathex

defp element(type, id) do
  Pathex.Lenses.star() ~> matching(%{type: ^type, id: ^id})
end
defp children do
  path(:children)
end

def add_tag(elements, id, parent_id, layout_id, tag, type) do
  lens = element("layout", layout_id) ~> children ~> element("section", parent_id) ~> children ~> element(type, id)
  Pathex.over(elements, lens, fn element -> Map.put(element, :tag, tag) end)
end

You can learn how to create you own lenses with Pathex using these 5min tutorials:

  1. basics
  2. lenses
  3. cheatsheet
3 Likes

My best try to explain :smiling_face_with_tear: sorry I am not native in english

That is why I added the order list in my suggestion. You can separate your description of the children and information about their order.

Let’s say you have order of UUIDs ["text-1", "text-2", "text-3"], when a user moves the div with ID text-1 down, you can do the following (SwappableList is defined at the end):

order = ["text-1", "text-2", "text-3"]
# Place text-1 after text-2
SwappableList.move_after(order, "text-1", after: "text-2"))

For your usecase, you could have a method that takes elements, the path to the parent, the id you want to move and the location you want to put it after.

# move(elements, "layout", "parent", "text-1", "text-2")
def move(elements, layout_id, parent_id, id, after_id) do
   update_in(elements,  [:children, layout_id,:children, parent_id, :order], fn order ->
    SwappableList.move_after(order, id, after: after_id))
  end)
end


SwappableList
defmodule SwappableList do
  @doc ~S"""
  Inserts 

  ## Examples

      # if item_before is nil, move to the front of the list
      iex> SwappableList.move_after([1,2,3], 0, after: nil)
      [0,1,2,3]
      
      iex> SwappableList.move_after([1,2,3], 0, after: 2)
      [1,2,0,3]
      
      # If item already in list, change its location
      iex> SwappableList.move_after([1,2,3], 0, after: 4)
      [1,2,3]

      # Delete all existing occurences
      iex> SwappableList.move_after([1,2,3], 2, after: 3)
      [1,3,2]

      iex> SwappableList.move_after([1,2,2,3], 2, after: 3)
      [1,3,2]

      iex> SwappableList.move_after([1,2,3], 3, after: 1)
      [1,3,2]

  """
  def move_after(list, new_item, after: item_before) do
    do_move_after([], list, item_before, new_item)
  end

  defp do_move_after(_, list, nil, new_item) do
    [new_item | list]
  end

  defp do_move_after(head, [item_before | tail], item_before, new_item) do
    Enum.reverse(head) ++ [item_before, new_item | Enum.reject(tail, &(&1 == new_item))]
  end

  defp do_move_after(head, [], _, new_item) do
    Enum.reverse(head)
  end

  defp do_move_after(head, [another_item | tail], item_before, new_item)
       when another_item == new_item do
    do_move_after(head, tail, item_before, new_item)
  end

  defp do_move_after(head, [another_item | tail], item_before, new_item) do
    do_move_after([another_item | head], tail, item_before, new_item)
  end
end
1 Like

Super interesting, Lenses are familiar from Haskell, but I’ve never seen them used in Elixir!

1 Like

Oh I need to try this, Thank you

1 Like

Can confirm that pathex is great. It’s one of the libraries that I add to all my projects (almost : except when working on libs where dependencies should be minimised, even then I’d add pathex if necessary).

2 Likes

Yes sure, I want to try it, but I do not want to be dropped inside magic; so I need to review the code and learn some concept this library use.
At the beginning of my project I really needed to keep my project size very small but this is taking up a lot of my time.
Thank you for the confirming

Pathex is just like Elixir’s builtin Access, but structure-friendly, user-friendly, more performant and with some other cool features. They both use the same approach called “Functional optics”. Long story short, this approach is about writing fn functions which can set/get/update value in a structure (like getters or setter in OOP languages), and libraries like Pathex or Access are providing interface to create these functions and compose them together.

This approach is really useful and I suggest everyone to learn some optics, because it is universal way to traverse any nested structure. There are XPaths for XML, there are CSS selectors for HTML, there is dot-notation for structures, there is Access for primitive structures, but these approaches are format-specific, which is strange since everything in Elixir is represented as a combination of maps, lists and tuples. So, Pathex just leverages this feature and significantly reduces amount of stuff one must know to traverse nested data in Elixir. Just master Pathex and you can traverse XML, HTML, nested JSON or anything else using one tool.

So, there’s no magic, there are just functions, hehe

4 Likes

agree, only that it is most of the time way easier to work in a flat structure. May be a little slower to render, but will not be relevant in this use case.

1 Like