Since in your question you mention HTML table I’m giving a full example script using phoenix_html dependency to generate HTML code.
Mix.install([:phoenix_html])
defmodule Example do
alias Phoenix.HTML
alias Phoenix.HTML.Tag
def sample(list), do: build_table(list)
defp build_table(list) do
:table
|> Tag.content_tag do
[build_thead(), build_tbody(list)]
end
|> HTML.safe_to_string()
end
defp build_thead do
Tag.content_tag :thead do
Tag.content_tag :tr do
Enum.map(~w"Foos Bars FooBars", &Tag.content_tag(:th, &1))
end
end
end
defp build_tbody(list) do
Tag.content_tag :tbody do
Enum.map(list, &build_tbody_foo/1)
end
end
defp build_tbody_foo(%{bars: bars, text: text}) do
bars = update_in(bars, [Access.at(0), :foo_bars, Access.at(0)], &Map.put(&1, :add_foo, true))
foo_span = count_foo_span(bars)
Enum.map(bars, &build_tbody_bars(&1, text, foo_span))
end
defp build_tbody_bars(%{foo_bars: foo_bars, text: text}, foo_text, foo_span) do
foo_bars = update_in(foo_bars, [Access.at(0)], &Map.put(&1, :add_bar, true))
span = count_foo_bar_span(foo_bars)
Enum.map(foo_bars, fn foo_bar ->
contents = build_tbody_foo_bars(foo_bar, foo_text, foo_span, text, span)
Tag.content_tag(:tr, contents)
end)
end
defp build_tbody_foo_bars(%{add_foo: true} = foo_bar, foo_text, foo_span, bar_text, bar_span) do
foo_bar = Map.delete(foo_bar, :add_foo)
[
Tag.content_tag(:td, foo_text, rowspan: foo_span),
Tag.content_tag(:td, bar_text, rowspan: bar_span) | do_build_tbody_foo_bars(foo_bar)
]
end
defp build_tbody_foo_bars(%{add_bar: true} = foo_bar, _foo_text, _foo_span, bar_text, bar_span) do
foo_bar = Map.delete(foo_bar, :add_bar)
[Tag.content_tag(:td, bar_text, rowspan: bar_span) | do_build_tbody_foo_bars(foo_bar)]
end
defp build_tbody_foo_bars(foo_bar, _foo_text, _foo_span, _bar_text, _bar_span) do
do_build_tbody_foo_bars(foo_bar)
end
defp do_build_tbody_foo_bars(%{text: text}) do
Tag.content_tag(:td, text, rowspan: 1)
end
defp count_foo_span(bars) do
count = bars |> Enum.map(&Enum.count(&1.foo_bars)) |> Enum.sum()
(count == 0 && 1) || count
end
defp count_foo_bar_span(foo_bars) do
count = Enum.count(foo_bars)
(count == 0 && 1) || count
end
end
input = [
%{
text: "Foo 1",
bars: [
%{
text: "Bar 1-1",
foo_bars: [
%{text: "FooBar 1-1-1"},
%{text: "FooBar 1-1-2"},
%{text: "FooBar 1-1-3"},
%{text: "FooBar 1-1-4"}
]
},
%{
text: "Bar 1-2",
foo_bars: [
%{text: "FooBar 1-2-1"},
%{text: "FooBar 1-2-2"}
]
}
]
},
%{
text: "Foo 2",
bars: [
%{
text: "Bar 2-1",
foo_bars: [
%{text: "FooBar 2-1-1"}
]
},
%{
text: "Bar 2-2",
foo_bars: [
%{text: "FooBar 2-2-1"},
%{text: "FooBar 2-2-2"},
%{text: "FooBar 2-2-3"}
]
}
]
}
]
Example.sample(input)
|> IO.puts()
Here is a result based on your input:
<table><thead><tr><th>Foos</th><th>Bars</th><th>FooBars</th></tr></thead><tbody><tr><td rowspan="6">Foo 1</td><td rowspan="4">Bar 1-1</td><td rowspan="1">FooBar 1-1-1</td></tr><tr><td rowspan="1">FooBar 1-1-2</td></tr><tr><td rowspan="1">FooBar 1-1-3</td></tr><tr><td rowspan="1">FooBar 1-1-4</td></tr><tr><td rowspan="2">Bar 1-2</td><td rowspan="1">FooBar 1-2-1</td></tr><tr><td rowspan="1">FooBar 1-2-2</td></tr><tr><td rowspan="4">Foo 2</td><td rowspan="1">Bar 2-1</td><td rowspan="1">FooBar 2-1-1</td></tr><tr><td rowspan="3">Bar 2-2</td><td rowspan="1">FooBar 2-2-1</td></tr><tr><td rowspan="1">FooBar 2-2-2</td></tr><tr><td rowspan="1">FooBar 2-2-3</td></tr></tbody></table>
here is its beautified version:
<table>
<thead>
<tr>
<th>Foos</th>
<th>Bars</th>
<th>FooBars</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="6">Foo 1</td>
<td rowspan="4">Bar 1-1</td>
<td rowspan="1">FooBar 1-1-1</td>
</tr>
<tr>
<td rowspan="1">FooBar 1-1-2</td>
</tr>
<tr>
<td rowspan="1">FooBar 1-1-3</td>
</tr>
<tr>
<td rowspan="1">FooBar 1-1-4</td>
</tr>
<tr>
<td rowspan="2">Bar 1-2</td>
<td rowspan="1">FooBar 1-2-1</td>
</tr>
<tr>
<td rowspan="1">FooBar 1-2-2</td>
</tr>
<tr>
<td rowspan="4">Foo 2</td>
<td rowspan="1">Bar 2-1</td>
<td rowspan="1">FooBar 2-1-1</td>
</tr>
<tr>
<td rowspan="3">Bar 2-2</td>
<td rowspan="1">FooBar 2-2-1</td>
</tr>
<tr>
<td rowspan="1">FooBar 2-2-2</td>
</tr>
<tr>
<td rowspan="1">FooBar 2-2-3</td>
</tr>
</tbody>
</table>
and finally here is a simple fiddle for human friendly preview:
Ok, so what’s the difference?
- First of all I’m not building data ready to render, but collecting data when needed for rendering instead. With this you do not need to write another loop to render your data.
- I’m not using any extra
flatten functions here. Look that nested iodata is handled already by Phoenix code. For readability I have called Phoenix.HTML.safe_to_string/1, so by removing this call you will have raw safe code.
- Because of 1st point I can have all spans and texts from
foos and bars in separate variable i.e. I do not need to add those values to your input.
- The only thing I’m doing is to quickly put
add_foo and add_bar boolean values, so I can use simple pattern matching on those and render foos and bars data only when needed (for first foos and bars rows respectively).
Note: First foos row is also first bars row. Therefore I’m adding also bar cell when pattern matching on add_foo row.
Helpful resources:
- Phoenix.HTML.safe_to_string/1
- Phoenix.HTML.Tag.content_tag/2
- phoenix_html at hex.pm