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_nested
method- Source data β hash array
- Source data β ActiveRecord array
-
.to_nested
method optionsroot_id: id
branch_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_options
method - Thin pseudographics
- Own pseudographics
- Increase indentation in own pseudographics
- Adding pseudo-graphics before the model name using the
- In html forms
- With the
form.select
helper - With
form.select
andoptions_for_select
helpers - 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_inspect
This 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_inspect
This 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"