Relaxed operators w/ LiveView functional components

Starting with the line,

<%= if !!@initials && render_slot(@svg) do %>

In the module directly below:

I’m curious why I wasn’t able to make a relaxed operator work, like

{render_slot(@svg) || !!@operator && SOME_CODE }

I tried making the default svg a functional component, plain text, nested sigil_H (not even sure if that would work in any case?).

defmodule ContentAgentWeb.TailwindUi.Components.AvatarComponent do
  use ContentAgentWeb, :html
  import Phoenix.LiveView.JS

  attr :src, :string, default: nil, doc: "the image source for the avatar"
  attr :square, :boolean, default: false, doc: "whether the avatar is square or circular"
  attr :initials, :string, default: nil, doc: "the initials to display if no image is provided"
  attr :alt, :string, default: "", doc: "the alt text for the avatar"
  attr :class, :string, default: "", doc: "additional classes for the avatar"
  attr :rest, :global, doc: "the attributes for the avatar"

  slot :svg, required: false, doc: "the slot for the svg element" do
    attr :initials, :string, doc: "Initials to display"
    attr :alt, :string, doc: "Alt text for the SVG"
  end

  def avatar(assigns) do
    ~H"""
    <span
      {@rest}
      class={[
        @class,
        "inline-grid shrink-0 align-middle [--avatar-radius:20%] [--ring-opacity:20%] *:col-start-1 *:row-start-1",
        "outline outline-1 -outline-offset-1 outline-black/[--ring-opacity] dark:outline-white/[--ring-opacity]",
        (@square && "rounded-[--avatar-radius] *:rounded-[--avatar-radius]") ||
          "rounded-full *:rounded-full"
      ]}
    >
      <%= if !!@initials && render_slot(@svg) do %>
        {render_slot(@svg)}
      <% else %>
        <%= if !!@initials do %>
          <svg
            :if={!!@initials}
            class="size-full select-none fill-current p-[5%] text-[48px] font-medium uppercase"
            viewBox="0 0 100 100"
            aria-hidden={@alt != ""}
          >
            <title :if={@alt != ""}>{@alt}</title>
            <text
              x="50%"
              y="50%"
              alignment-baseline="middle"
              dominant-baseline="middle"
              text-anchor="middle"
              dy=".125em"
            >
              {@initials}
            </text>
          </svg>
        <% end %>

        <img :if={@src} class="size-full" src={@src} alt={@alt} />
      <% end %>
    </span>
    """
  end
end

Just curious about the underlying reason why that is the case when I was able to get stuff like this to work:


  attr :color, :atom, default: :dark_zinc
  attr :base_class, :list, default: @base_classes
  attr :button_style, :atom, default: :primary, values: [:primary, :secondary, :soft]
  attr :button_text, :string
  attr :class, :list, default: []
  attr :disabled, :boolean, default: false
  attr :type, :string, default: "button"
  attr :rest, :global
  slot :icon
  slot :inner_block

  def button(assigns) do
    color_classes = color_classes(%{color: assigns.color})
    button_style = button_style(%{button_style: assigns.button_style})

    assigns =
      assign(
        assigns,
        :computed_classes,
        [button_style | [color_classes | [assigns.class | [assigns.base_class]]]]
      )

    assigns =
      (assigns.icon &&
         assign(
           assigns,
           :icon,
           "<div data-slot=\"icon\"> <%= render_slot(@icon) %> </div>"
         )) || assigns

    ~H"""
    <button type={@type} class={@computed_classes} disabled={@disabled} {@rest}>
      <span
        class="absolute left-1/2 top-1/2 size-[max(100%,2.75rem)] -translate-x-1/2 -translate-y-1/2 [@media(pointer:fine)]:hidden"
        aria-hidden="true"
      />
      {@icon}
      {render_slot(@inner_block) || @button_text} <<<<<-----------------------
    </button>
    """
  end

  attr :src, :string, default: nil, doc: "the image source for the avatar"
  attr :square, :boolean, default: false, doc: "whether the avatar is square or circular"
  attr :initials, :string, default: nil, doc: "the initials to display if no image is provided"
  attr :alt, :string, default: "", doc: "the alt text for the avatar"
  attr :class, :string, default: "", doc: "additional classes for the avatar button"
  attr :rest, :global, doc: "the attributes for the avatar button"

  def avatar_button(assigns) do
    assigns =
      assign(assigns, :classes, [
        {(assigns.square && "rounded-[20%]") || "rounded-full"}, <<<<<-----------------------
        "relative inline-grid focus:outline-none data-[focus]:outline data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500"
        | [assigns.class]
      ])

    ~H"""
    <button
      class={Enum.join(@classes, " ")}
      phx-hover={JS.add_class("data-hover")}
      phx-mouse-leave={JS.remove_class("data-hover")}
      phx-focus={JS.add_class("data-focus")}
      phx-blur={JS.remove_class("data-focus")}
      phx-click={
        JS.add_class("data-active", to: "button")
        |> JS.remove_class("data-active", to: "button", delay: 200)
      }
      {@rest}
    >
      <span
        class="absolute left-1/2 top-1/2 size-[max(100%,2.75rem)] -translate-x-1/2 -translate-y-1/2 [@media(pointer:fine)]:hidden"
        aria-hidden="true"
      />
      <AvatarComponent.avatar src={@src} square={@square} initials={@initials} alt={@alt}>
      </AvatarComponent.avatar>
    </button>
    """
  end

Also, always looking to improve so if there are ways to improve what I’m trying to do here, always open to feedback if you have time to share feedback :slight_smile:

Sorry, but why would you put render_slot in an if statement?

That template is a bit untidy. You can have separate function heads for the image or initials, or have a default for when the slot is not provided. You can check if slot == [].

I’m refactoring some React components as an exercise.

Sorry, but why would you put render_slot in an if statement?

The overall thing I’m trying to accomplish:

If @initial and there’s a svg slot, render the svg slot
if @initial is set and there’s not a svg slot, then show a default svg
If @initial isn’t set, don’t show anything.

I believe you might be confused about how to check for empty slots. Slots are always lists, and you can determine if a slot is not provided by comparing it to an empty list. I would recommend refactoring your code as follows:

defmodule ContentAgentWeb.TailwindUi.Components.AvatarComponent do
  use ContentAgentWeb, :html
  import Phoenix.LiveView.JS

  attr :src, :string, default: nil, doc: "the image source for the avatar"
  attr :square, :boolean, default: false, doc: "whether the avatar is square or circular"
  attr :initials, :string, default: nil, doc: "the initials to display if no image is provided"
  attr :alt, :string, default: "", doc: "the alt text for the avatar"
  attr :class, :string, default: "", doc: "additional classes for the avatar"
  attr :rest, :global, doc: "the attributes for the avatar"

  slot :svg, required: false, doc: "the slot for the svg element" do
    attr :initials, :string, doc: "Initials to display"
    attr :alt, :string, doc: "Alt text for the SVG"
  end

   def avatar(assigns) do
    ~H"""
    <span
      {@rest}
      class={[
        @class,
        "inline-grid shrink-0 align-middle [--avatar-radius:20%] [--ring-opacity:20%] *:col-start-1 *:row-start-1",
        "outline outline-1 -outline-offset-1 outline-black/[--ring-opacity] dark:outline-white/[--ring-opacity]",
        (@square && "rounded-[--avatar-radius] *:rounded-[--avatar-radius]") ||
          "rounded-full *:rounded-full"
      ]}
    >
      <%= cond do %>
        <% @svg != [] -> %>
          {render_slot(@svg)}
        <% not is_nil(@initials) -> %>
          <svg
            class="size-full select-none fill-current p-[5%] text-[48px] font-medium uppercase"
            viewBox="0 0 100 100"
            aria-hidden={@alt != ""}
          >
            <title :if={@alt != ""}>{@alt}</title>
            <text
              x="50%"
              y="50%"
              alignment-baseline="middle"
              dominant-baseline="middle"
              text-anchor="middle"
              dy=".125em"
            >
              {@initials}
            </text>
          </svg>
        <% true -> %>
          <%!-- Nothing --%>

      <% end %>
      <img :if={@src} class="size-full" src={@src} alt={@alt} />
    </span>
    """
  end
end

My understanding was that render_slot returns ‘nil’ when the slot list is empty. Thank you for your help. I’ll be sure to do it that way.

1 Like

Yes, but rendering the slot and checking if it’s nil is much more costly than comparing it to a list.

1 Like

That makes complete sense. Thanks!

1 Like