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