nested_array
🎉 Мои поздравления! Вышла версия 3.0.
Гем nested_array
позволяет преобразовать плоский массив данных древовидной
структуры во вложенный массив, а так же помогает отобразить деревья формируя
HTML вёрстку или псевдографику.
Древовидная структура должна быть описана по шаблону Списка смежности (Adjacency List), то есть в каждом узле указан предок.
Выбрать язык README.md
Оглавление
- Установка
- Использование
Установка ↑
- Добавте строку в файл Gemfile вашего приложения:
# Работа с древовидными массивами
gem "nested_array", "~> 3.0"
И выполните bundle install
.
- Если вы планируете использовать скромные CSS стили гема, добавте в файл app/assets/stylesheets/application.scss:
/* Отображение древовидных массивов */
@import "nested_array";
Использование ↑
Преобразование данных методом .to_nested
↑
Исходные данные – массив хэш ↑
Допустим, есть массив хэш:
flat = [
{'id' => 3, 'parent_id' => nil},
{'id' => 2, 'parent_id' => 1},
{'id' => 1, 'parent_id' => nil}
]
Где каждый хэш это узел дерева, id
— идентификатор узла,
parent_id
— указатель на родительский узел.
Необходимо преобразовать в массив в котором будут только корневые узлы
('parent_id' => nil
), а дочерние узлы помещены в поле
children
.
nested = flat.to_nested
puts nested.pretty_inspect
Выведет:
[#<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}>]
В результате узлы представляют собой объекты OpenStruct
у которых
исходные поля id
, parent_id
и дополнительные поля
level
, origin
и children
.
В качестве исходных узлов могут быть и объекты ActiveRecord.
Исходные данные – массив ActiveRecord ↑
catalogs = Catalog.all.to_a
nested = catalogs.to_nested
puts nested.pretty_inspect
Выведет:
[
#<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>>
]
Метод .to_nested
использует метод object.serializable_hash
, чтобы получить список полей объекта.
Опции метода .to_nested
↑
root_id: id
↑
root_id: 1
— взять потомков узла с id
равным 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>
Выведет многоуровневый маркированный список потомков узла №1:
branch_id: id
↑
branch_id: 1
— взять узел с id
равным 1
и всех его потомков.
<% 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>
Выведет узел №1 и его потомков:
Отображение древовидных структур ↑
В виде многоуровневых списков ↑
Маркированный и нумерованный списки <ul>
, <ol>
↑
<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>
Использование собственных шаблонов для отображения списка ↑
Вместо <ul><li>
/<ol><li>
<% content_for :head do %>
<style>
/* Вертикальные отступы узла */
div.li { margin: .5em 0; }
/* Отступ уровней (children) */
div.ul { margin-left: 2em; }
</style>
<% end %>
<div class="ul">
<%# Переопределение открывающих и закрывающих тегов шаблонов. %>
<% @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>
Изменение шаблона в зависимости от данных узла ↑
Для изменения шаблонов вывода в зависимости от данных узла мы можем проверять
поля узла node.li
и node.ul
. Если поля не пустые, то вместо вывода их
содержимого подставлять собственный динамичный html.
Вывод имеющихся шаблонов узла (node.li
, node.ul
и 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>
Замена шаблонов на динамический 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>
Стоит отметить, что поле node.li
всегда присутствует в узле, в отличие от
node.ul
.
Расскрывающийся список на основе тега <details></details>
↑
<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>
По умолчанию подуровни скрыты, можно управлять отображением подуровней передавая
опцию в метод узла: 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>
Формирование и вывод собственных шаблонов опираясь на изменение уровня узла node.level
↑
<% 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| %>
<%# Уровень повысился? — открываем подуровень. %>
<% if prev_level.present? && prev_level < node.level %>
<div class="children">
<% end %>
<%# Уровень тот же? — предыдущий закрываем просто. %>
<% if prev_level.present? && prev_level == node.level %>
</div>
<% end %>
<%# Уровень понизился? - предыдущий закрываем сложно. %>
<% if prev_level.present? && prev_level > node.level %>
<% (prev_level - node.level).times do |t| %>
</div>
</div>
<% end %>
</div>
<% end %>
<%# Наш узел. %>
<div class="node">
<%= origin.name %>
<% prev_level = node.level %>
<% end %>
<%# Учёт предыдущего уровня при выходе из цикла (Уровень понизился). %>
<% if !prev_level.nil? %>
<% prev_level.times do |t| %>
</div>
</div>
<% end %>
</div>
<% end %>
</div>
В виде псевдографики ↑
Добавление псевдографики перед именем модели методом nested_to_options
↑
<% 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.select
↑
<%= 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.select
и options_for_select
↑
<%= 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.radio_button
↑
<%= 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 %>
Разработка
Для подключения локальной версии гема замените в строке подключения (файл Gemfile) второй аргумент (версию) на опцию path:
# Gemfile
# Работа с древовидными массивами
gem "nested_array", path: "../nested_array"
Часто используемые команды