Combining HTML classes with initial values in Phoenix Components

Hey, everyone.

I’ve been working with Phoenix Components for a few weeks, and they are great. One thing I use a lot when building custom components is the global attribute with default values set. It’s a good option for building reusable elements without repeating stuff every time.

However, I’ve noticed a “pattern” when creating components with class modifiers.

Suppose I have a <.paragraph />:

attr :rest, :global, default: %{class: "paragraph"}
slot :inner_block, required: true

def paragraph(assigns) do
  ~H"""
  <p {@rest}><%= render_slot(@inner_block) %></p>
  """
end
<.paragraph>My paragraph</.paragraph>
<p class="paragraph">My paragraph</p>

The problem is that I have modifiers for my paragraph, like paragraph--medium, paragraph--small, or even other types of classes like is-hidden, is-animateable etc. To use them, I need to repeat its base class.

<.paragraph class="paragraph paragraph--with-modifier">
  This is a paragraph with a modifier
</.paragraph>
<p class="paragraph paragraph--with-modifier">My paragraph</p>

Of course, changing how things currently work because I don’t want to repeat a class is not a good idea, and I understand the default value is not the best option for what I’m trying to do. But things can get more complex as we have more than a single base class.

Also, I don’t think creating additional components to consider all the modifiers they could have is a good idea. It’s just simpler to use classes.

Now, to the suggestion: what if we had something like an initial property for the class in global attributes? Values from it would be combined with the values passed to the component automatically.

attr :rest, :global, initial: %{class: "paragraph is-animateable is-i-dont-know"}
slot :inner_block, required: true

def paragraph(assigns) do
  ~H"""
  <p {@rest}><%= render_slot(@inner_block) %></p>
  """
end
<.paragraph class="paragraph--with-modifier">
  This is my paragraph with modifiers
</.paragraph>
<p class="paragraph is-animateable is-i-dont-know paragraph--with-modifier">This is my paragraph with modifier</p>

I read this article, which is a great workaround, but maybe we can improve it.

What do you all think?

Thanks!

attr :class, :string
attr :rest, :global
slot :inner_block, required: true

def paragraph(assigns) do
  ~H"""
  <p class={["paragraph is-animateable is-i-dont-know", @class]} {@rest}>
    <%= render_slot(@inner_block) %>
  </p>
  """
end

I mentioned your solution in the post, @LostKobrakai.

Ah, didn’t look at that one. I wouldn’t consider this a workaround. That’s imo the business logic of your component. Sure initial_value sounds easy, but you can compose passed data with defaults in multiple ways, not just one. How would you decide if the syntax you showed combines values or replaces the default or does some more complex transformation? Even combining values is not a trivial assumption to make, as it might work well for classes or styles, but unlikely for much else.

1 Like

{@rest} could be tweaked inside the render before returning ~H""" like is done in core_components:


  def input(%{type: "checkbox", value: value} = assigns) do
    assigns =
      assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)

to tweak the list of classes based on availability of another class.

1 Like

I agree. There are multiple ways we can accomplish that. My point is: should we have something out-of-the-box for this case?

Yes. Classes/styles/data-attributes are probably the only use-cases for this as they can have multiple values.