tomekowal
Would you find ComposableHtml library useful?
I’ve recently found myself writing many Phoenix templates and wanted to streamline my experience.
I often want to set some defaults, e.g. in Bootstrap a table usually has a class “table” <table class="table"> But then I’d like to take the default and specify that this time the table should be striped.
I can’t use Phoenix.HTML.Tag.content_tag in this case. Everything in Phoenix.HTML produces final io lists that can’t be modified later. I figured I need some middleware.
defmodule ComposableHtml.Tag do
defstruct [:tag_name, :content, :attrs]
end
This way I can define a tag and extend it:
table = %Tag{tag_name: :table, content: ..., attrs: [class: "table"]}
...
table_striped = tag |> update_attr(:class, &"#{&1} table-striped")
I saw the same problem in the Elm community https://www.youtube.com/watch?v=PDyWP-0H4Zo
The talk concludes that a data structure that allows overwriting its fields is a nice API.
I’d like to turn my helpers into a lib generic enough to be a building block for other libraries like API Reference — ExEffectiveBootstrap v0.1.17
The idea is also to be able to use the ComposableHtml.Tag directly in the templates by implementing Phoenix.HTML.Safe protocol:
defmodule ComposableHtml.Tag do
...
def to_phoenix_html(%Tag{} = tag) do
if tag.content do
PhoenixTag.content_tag(tag.tag_name, to_phoenix_html(tag.content), tag.attrs)
else
PhoenixTag.tag(tag.tag_name, tag.attrs)
end
end
def to_phoenix_html(list_of_tags) when is_list(list_of_tags) do
Enum.map(list_of_tags, &to_phoenix_html/1)
end
# We pass the data in the `content_tag`, so we don't have to escape here
def to_phoenix_html(data), do: data
end
defimpl Phoenix.HTML.Safe, for: ComposableHtml.Tag do
alias ComposableHtml.Tag
def to_iodata(data) do
{:safe, data} = Tag.to_phoenix_html(data)
data
end
end
I’ve found existing HTML builder that uses macros GitHub - dczombera/html_builder: A small Elixir HTML library that uses its own DSL for easily creating HTML tags · GitHub
But it is not composable.
My questions are:
- Would you find such a library useful?
- If it is useful, are there any existing libraries for composing HTML that I missed?
- If I am going to create a new library, could you help me with API design?
Ad.3 The with_* approach from Brian’s talk seems compelling but it is more specific than I like. E.g. in the talk there is |> withRole Danger |> withFormat Outline and in Bootstrap they would both translate to a class <button class="btn btn-outline btn-danger">
I am not sure what would be some proper names for functions that append to an attribute (e.g. changing class="table" to class="table table-striped"), and what names to use for functions overwriting the attributes.
Most Liked
sfusato
There’s this new library that has just been announced which seems to be doing what you want (and more than that): x_component. If there’s something that I miss from Vue/React, it’s this kind of component composability. Looks very powerful (also check the benchmarks).
Then there’s also Surface which is aimed directly at Liveview (discussion).
tomekowal
Thank you! Those are very useful links!
Both libraries emphasize composability which makes me think I am on a good track ![]()
However, the scope of what I am thinking about is much much smaller. I don’t want to define new templating language.
Let me give a before and after example.
Let’s say I want to define a table helper for bootstrap:
In my view_helpers.ex I need this:
def bootstrap_table(content) do
content_tag(:table, content, class: "table")
end
In the template, I can use <%= bootstrap_table(@content) %>
But then in some other place, I need “table-striped” so I modify the herlper
def bootstrap_table(content, opts \\ []) do
additional_classes = Keyword.get(opts, :additional_classes, "")
content_tag(:table, content, class: "table #{additional_classes}")
end
But then in yet another place I need to set border, so I modify the helper:
def bootstrap_table(content, opts \\ []) do
additional_classes = Keyword.get(opts, :additional_classes, "")
border = Keyword.get(opts, :border, "0")
content_tag(:table, content, class: "table #{additional_classes}", border: border)
end
And before I realize, I add cellpadding, cellspacing and finally all attributes supported by table tag.
Let’s say that instead, I use ComposableHtml.Tag
def bootstrap_table(content) do
%Tag{tag_name: :table, content: content, attrs: [class: "table"]}
end
The usage in the template would look the same <%= bootstrap_table(content) %> provided that %Tag{} struct implements Phoenix.HTML.Safe.
But now, if I need to add a class, I can do it differently:
def striped(table_tag) do
ComposableHtml.add_class(table_tag, "table-striped")
end
def bordered(table_tag) do
ComposableHtml.add_attribute(:border, "1")
end
Now in the template, I can do <%= bootstrap_table(content) |> striped() |> bordered() %>
Another nice example is what @wojtekmach wants here: Proposal: conditionally setting css classes · Issue #276 · phoenixframework/phoenix_html · GitHub
Let’s say you want to conditionally add active class based on current path:
<%= link_to("Home", "/", class: (["button"] ++ (if path == "/", do: ["active"], else: [])) %>
If link_to returned ComposableHtml.Tag, you could write:
<%= link_to("Home", "/", class: "button") |> maybe_add_active_class(path, "/") %>
and define a helper:
def maybe_add_active_class(tag, path, active_path) do
if path == active_path do
ComposableHtml.add_class(tag, "active")
else
tag
end
end
I’d like the library to make HTML composable in a similar way that Ecto.Query makes SQL composable ![]()
tomekowal
I think I would start with a separate library to test how it feels and what would be a nice API. If it catches on and Crhis likes the idea, it could become part of Phoenix.HTML ![]()
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









