nested_array
π Congratulations! Version 3.0 has been released.
Gem nested_array allows you to convert a flat data array with a tree structure
into a nested array. It also helps to display trees by forming HTML layout or
pseudo-graphics.
The tree structure must be described using the Adjacency List pattern, that is, each node has an ancestor.
Select language README.md
- en English
 - ru Π ΡΡΡΠΊΠΈΠΉ
 
- Installation
 - Usage
- Converting data using the 
.to_nestedmethod- Source data β hash array
 - Source data β ActiveRecord array
 - 
.to_nestedmethod optionsroot_id: idbranch_id: id
 
 - Displaying tree structures
- As multi-level lists
- Bulleted and numbered lists 
<ul>,<ol> - Using your own templates to display a list
 - Changing the template depending on node data
 - Dropdown list based on 
<details></details>tag - Formation and output of your own templates based on changing the node level 
node.level 
 - Bulleted and numbered lists 
 - Pseudo-graphic output
- Adding pseudo-graphics before the model name using the 
nested_to_optionsmethod - Thin pseudographics
 - Own pseudographics
 - Increase indentation in own pseudographics
 
 - Adding pseudo-graphics before the model name using the 
 - In html forms
- With the 
form.selecthelper - With 
form.selectandoptions_for_selecthelpers - Dropdown list with radio buttons 
form.radio_button 
 - With the 
 
 - As multi-level lists
 
 - Converting data using the 
 
- Add a line to your application's Gemfile:
 
# Working with tree arrays
gem "nested_array", "~> 3.0"And do bundle install.
- If you plan to use modest CSS gem styles, add to the app/assets/stylesheets/application.scss file:
 
/* Displaying Tree Arrays */
@import "nested_array";Let's say we have a hash array:
flat = [
  {'id' => 3, 'parent_id' => nil},
  {'id' => 2, 'parent_id' => 1},
  {'id' => 1, 'parent_id' => nil}
]Where each hash is a tree node, id is the node identifier, parent_id is a
pointer to the parent node.
It is necessary to convert it into an array in which there will be root nodes
('parent_id' => nil), and child nodes placed in the children field.
nested = flat.to_nested
puts nested.pretty_inspectThis will output:
[#<OpenStruct id=3, parent_id=nil, level=0, origin={"id"=>3, "parent_id"=>nil}>,
 #<OpenStruct id=1, parent_id=nil, level=0, children=[#<OpenStruct id=2, parent_id=1, level=1, origin={"id"=>2, "parent_id"=>1}>], origin={"id"=>1, "parent_id"=>nil}>]
As a result, nodes are OpenStruct objects with initial fields id, parent_id and additional fields level, origin and children.
ActiveRecord objects can also serve as source nodes.
catalogs = Catalog.all.to_a
nested = catalogs.to_nested
puts nested.pretty_inspectThis will output:
[
  #<OpenStruct id=1, parent_id=nil, level=0, origin=#<Catalog id: 1, name: "Computer Components", parent_id: nil>, children=[
    #<OpenStruct id=11, parent_id=1, level=1, origin=#<Catalog id: 11, name: "External Components", parent_id: 1>, children=[
      #<OpenStruct id=111, parent_id=11, level=2, origin=#<Catalog id: 111, name: "Hard Drives", parent_id: 11>>,
      #<OpenStruct id=112, parent_id=11, level=2, origin=#<Catalog id: 112, name: "Sound Cards", parent_id: 11>>,
      #<OpenStruct id=113, parent_id=11, level=2, origin=#<Catalog id: 113, name: "KVM Switches", parent_id: 11>>,
      #<OpenStruct id=114, parent_id=11, level=2, origin=#<Catalog id: 114, name: "Optical Drives", parent_id: 11>>
    ]>,
    #<OpenStruct id=12, parent_id=1, level=1, origin=#<Catalog id: 12, name: "Internal Components", parent_id: 1>>
  ]>,
  #<OpenStruct id=2, parent_id=nil, level=0, origin=#<Catalog id: 2, name: "Monitors", parent_id: nil>>,
  #<OpenStruct id=3, parent_id=nil, level=0, origin=#<Catalog id: 3, name: "Servers", parent_id: nil>>,
  #<OpenStruct id=4, parent_id=nil, level=0, origin=#<Catalog id: 4, name: "Networking Products", parent_id: nil>>
]
The .to_nested method uses the object.serializable_hash method to get a list of the object's fields.
root_id: 1 β take children of node with id equal to 1.
<% catalogs_of_1 = Catalog.all.to_a.to_nested(root_id: 1) %>
<ul>
  <% catalogs_of_1.each_nested do |node, origin| %>
    <%= node.before -%>
    <%= origin.name -%> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after -%>
  <% end %>
</ul>Will output a multi-level bulleted list of descendants of node #1:
branch_id: 1 β take the node with id equal to 1 and all its descendants.
<% catalogs_from_1 = Catalog.all.to_a.to_nested(branch_id: 1) %>
<ul>
  <% catalogs_from_1.each_nested do |node, origin| %>
    <%= node.before -%>
    <%= origin.name -%> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after -%>
  <% end %>
</ul>Will output node #1 and its descendants:
<ul>
  <% @catalogs.to_a.to_nested.each_nested do |node, origin| %>
    <%= node.before %>
    <%= link_to origin.name, origin %> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after %>
  <% end %>
</ul>
<ol>
  <% @catalogs.to_a.to_nested.each_nested ul: '<ol>', _ul: '</ol>' do |node, origin| %>
    <%= node.before %>
    <%= link_to origin.name, origin %> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after %>
  <% end %>
</ol>Instead of <ul><li>/<ol><li>
<% content_for :head do %>
  <style>
    /* Vertical node padding */
    div.li { margin: .5em 0; }
    /* Level indentation (children) */
    div.ul { margin-left: 2em; }
  </style>
<% end %>
<div class="ul">
  <%# Overriding opening and closing template tags. %>
  <% @catalogs.to_a.to_nested.each_nested(
    ul: '<div class="ul">',
    _ul: '</div>',
    li: '<div class="li">',
    _li: '</div>'
  ) do |node, origin| %>
    <%= node.before -%>
    <%= origin.name -%> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after -%>
  <% end %>
</div>To change the output patterns depending on the node data, we can check the node
fields node.li and node.ul. If the fields are not empty, then instead of
displaying their contents, substitute your own dynamic html.
Output of available node templates (node.li, node.ul and node._):
<ul>
  <% @catalogs.to_a.to_nested.each_nested do |node, origin| %>
    <%= node.li -%>
    <%= origin.name -%> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.ul -%>
    <%= node._ -%>
  <% end %>
</ul>Replacing templates with dynamic html:
<% content_for :head do %>
  <style>
    li.level-0 {color: red;}
    li.level-1 {color: green;}
    li.level-2 {color: blue;}
    li.has_children {font-weight: bold;}
    ul.big {border: solid 1px gray;}
  </style>
<% end %>
<ul>
  <% @catalogs.to_a.to_nested.each_nested do |node, origin| %>
    <li class="level-<%= node.level %> <%= 'has_children' if node.is_has_children %>">
    <%= origin.name %> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <% if node.ul.present? %>
      <ul class="<%= 'big' if node.children.length > 2 %>">
    <% end %>
    <%= node._ -%>
  <% end %>
</ul>It's worth noting that the node.li field is always present in a node, unlike
node.ul.
<ul class="nested_array-details">
  <% @catalogs.to_a.to_nested.each_nested details: true do |node, origin| %>
    <%= node.before %>
    <%= origin.name %> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after %>
  <% end %>
</ul>By default, sublevels are hidden; you can control the display of sublevels by passing an option to the node method: node.after(open: ...):
<ul class="nested_array-details">
  <% @catalogs.to_a.to_nested.each_nested details: true do |node, origin| %>
    <%= node.before %>
    <%= origin.name %> <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
    <%= node.after(open: node.is_has_children) %>
  <% end %>
</ul><% content_for :head do %>
  <style>
    div.children {margin-left: 1em;}
    div.node {position: relative;}
    div.node::before {
      position: absolute;
      content: "";
      width: 0px;
      height: 0px;
      border-top: 5px solid transparent;
      border-bottom: 5px solid transparent;
      border-left: 8.66px solid red;
      left: -9px;
      top: 3px;
    }
  </style>
<% end %>
<div class="children">
  <% prev_level = nil %>
  <% @catalogs.to_a.to_nested.each_nested do |node, origin| %>
    <%# Has the level increased? - open the sublevel. %>
    <% if prev_level.present? && prev_level < node.level %>
      <div class="children">
    <% end %>
    <%# Same level? β we simply close the previous one. %>
    <% if prev_level.present? && prev_level == node.level %>
      </div>
    <% end %>
    <%# Has the level dropped? - closing the previous one is difficult. %>
    <% if prev_level.present? && prev_level > node.level %>
      <% (prev_level - node.level).times do |t| %>
        </div>
        </div>
      <% end %>
      </div>
    <% end %>
    <%# Our node. %>
    <div class="node">
    <%= origin.name %>
    <% prev_level = node.level %>
  <% end %>
  <%# Taking into account the previous level when exiting the cycle (Level has decreased). %>
  <% if !prev_level.nil? %>
    <% prev_level.times do |t| %>
      </div>
      </div>
    <% end %>
    </div>
  <% end %>
</div><% options = @catalogs.to_a.to_nested.nested_to_options(:name, :id) %>
<pre><code><%= options.pluck(0).join($/) %>
</code></pre><% options = @catalogs.to_a.to_nested.nested_to_options(:name, :id, thin_pseudographic: true) %>
<pre><code><%= options.pluck(0).join($/) %>
</code></pre><% options = @catalogs.to_a.to_nested.nested_to_options(:name, :id, pseudographics: %w(β¬ β β β β   β)) %>
<pre><code><%= options.pluck(0).join($/).html_safe %>
</code></pre><% options = @catalogs.to_a.to_nested.nested_to_options(:name, :id, pseudographics: ['ββ¬', 'ββ', 'β ', ' β', ' β', '  ', ' β']) %>
<pre><code><%= options.pluck(0).join($/).html_safe %>
</code></pre><%= form_with(model: Catalog.find(11), url: root_path, method: :get) do |form| %>
  <%= form.select :parent_id,
    @catalogs.to_a.to_nested.nested_to_options(:name, :id),
    {
      include_blank: 'None'
    },
    {
      multiple: false,
      size: 11,
      class: 'form-select form-select-sm nested_array-select'
    }
  %>
<% end %><%= form_with(model: Catalog.find(11), url: root_path, method: :get) do |form| %>
  <%= form.select :parent_id,
    options_for_select(
      @catalogs.to_a.to_nested.nested_to_options(:name, :id).unshift(['None', '']),
      selected: form.object.parent_id.to_s
    ),
    {
    },
    {
      multiple: false,
      size: 11,
      class: 'nested_array-select'
    }
  %>
<% end %><%= form_with(model: nil, url: root_path, method: :get) do |form| %>
  <ul class="nested_array-details">
    <% @catalogs.to_a.to_nested.each_nested details: true do |node, origin| %>
      <%= node.before %>
      <%= form.radio_button :parent_id, origin.id %>
      <%= form.label :parent_id, origin.name, value: origin.id %>
      <small>[<%= origin.id %>, <%= origin.parent_id || :nil %>, <%= node.level %>]</small>
      <%= node.after(open: node.is_has_children) %>
    <% end %>
  </ul>
<% end %>Development
To connect the local version of the gem, replace the second argument(version) in the connection line (Gemfile file) with the path option:
# Working with tree arrays
gem "nested_array", path: "../nested_array"














