Include all `attrs` from a different component

Does anybody know a way of “inheriting” all attrs from a function component? So for example if I want to create a specific variant of the link component I’d like to have all attrs which link supports in my custom component. Right now, the only way I know is to copy them over, which is a little pain, especially if the upstream component changes.

Additionally, if I define the attrs I also need to pass each of them manually.

Would it be possible to create a macro which you can pass an existing component which can then be “spread” (like :global attributes) into the custom component?

  # I want to inherit these from link/1
  attr :navigate, :string, default: nil
  attr :patch, :string, default: nil
  attr :href, :any, default: nil
  attr :replace, :boolean, default: false
  # ...

  attr :active, :boolean, default: false
  attr :rest, :global, default: %{}

  slot :icon
  slot :inner_block

  def nav_link(assigns) do
    ~H"""
    <.link
      navigate={@navigate}
      patch={@patch}
      href={@href}
      replace={@replace}
      class={[
        "group flex items-center px-2 py-2 text-sm font-medium rounded-md border border-transparent",
        if(@active,
          do: "bg-gradient-to-b from-zinc-800 to-zinc-700 text-white border-zinc-600 shadow-lg",
          else:
            "text-zinc-300 hover:bg-zinc-700 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
        )
      ]}
      {@rest}
    >
      <span class="text-zinc-400 group-hover:text-zinc-300 mr-3 flex-shrink-0 h-6 w-6">
        <%= render_slot(@icon) %>
      </span>
      <%= render_slot(@inner_block) %>
    </.link>
    """
  end

What I’d love to have:

  # somehow embed attrs from another component
  attr :link_attrs, embed: &link/1

  attr :active, :boolean, default: false
  attr :rest, :global, default: %{}

  slot :icon
  slot :inner_block

  def nav_link(assigns) do
    ~H"""
    <.link
      class={[
        "group flex items-center px-2 py-2 text-sm font-medium rounded-md border border-transparent",
        if(@active,
          do: "bg-gradient-to-b from-zinc-800 to-zinc-700 text-white border-zinc-600 shadow-lg",
          else:
            "text-zinc-300 hover:bg-zinc-700 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
        )
      ]}
      {@link_attrs}
      {@rest}
    >
      <span class="text-zinc-400 group-hover:text-zinc-300 mr-3 flex-shrink-0 h-6 w-6">
        <%= render_slot(@icon) %>
      </span>
      <%= render_slot(@inner_block) %>
    </.link>
    """
  end

The only thing I can think of right now, is adding an attr of :map and include the original component attrs there. Downside is, that you won’t get any compiler warnings of incorrect attrs.

attr :link_attrs, :map, default: %{}

  attr :active, :boolean, default: false
  attr :rest, :global, default: %{}

  slot :icon
  slot :inner_block

  def nav_link(assigns) do
    ~H"""
    <.link
      class={[
        "group flex items-center px-2 py-2 text-sm font-medium rounded-md border border-transparent",
        if(@active,
          do: "bg-gradient-to-b from-zinc-800 to-zinc-700 text-white border-zinc-600 shadow-lg",
          else:
            "text-zinc-300 hover:bg-zinc-700 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
        )
      ]}
      {@link_attrs}
      {@rest}
    >
      <span class="text-zinc-400 group-hover:text-zinc-300 mr-3 flex-shrink-0 h-6 w-6">
        <%= render_slot(@icon) %>
      </span>
      <%= render_slot(@inner_block) %>
    </.link>
    """
  end

Would it be possible to create a macro which you can pass an existing component which can then be “spread” (like :global attributes) into the custom component?

Can you? I don’t see why not. Should you? Certainly not.

Rust Koans​​​​​ - #4 by DanielKeep - The Rust Programming Language Forum My favorite link ever

If your downstream doesn’t care about the attrs specifically to handle something, just leave it in the global attr and hand it straight to your upstream that does care about it to define the attr. If both care about it define it explicitly, use it and explicitly pass it through to the parent, don’t rely on some macro magic to hide a minimal amount of duplication. When these two components do converge (and they will) you will have tightly coupled the two attr definitions together in the same place.

It’s easy to be too smart for your own good with macros in a language and too often than not you’ll find you can achieve the same result in a much more simple manner by using the default constructs.

3 Likes