tomekowal

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:

  1. Would you find such a library useful?
  2. If it is useful, are there any existing libraries for composing HTML that I missed?
  3. 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

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

tomekowal

Thank you! Those are very useful links!

Both libraries emphasize composability which makes me think I am on a good track :slight_smile:

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 :slight_smile:

tomekowal

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 :slight_smile:

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement