lib/petal_components/table.ex

defmodule PetalComponents.Table do
  use Phoenix.Component

  import PetalComponents.Avatar

  @doc ~S"""
  Renders a table with generic styling.

  ## Examples

      <.table id="users" rows={@users}>
        <:col :let={user} label="id"><%= user.id %></:col>
        <:col :let={user} label="username"><%= user.username %></:col>
        <:empty_state>No data here yet</:empty_state>
      </.table>
  """
  attr :id, :string
  attr :class, :any, default: nil, doc: "CSS class"
  attr :variant, :string, default: "basic", values: ["ghost", "basic"]
  attr :rows, :list, default: [], doc: "the list of rows to render"
  attr :row_id, :any, default: nil, doc: "the function for generating the row id"
  attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"

  attr :row_item, :any,
    default: &Function.identity/1,
    doc: "the function for mapping each row before calling the :col slot"

  slot :col do
    attr :label, :string
    attr :class, :any
    attr :row_class, :any
  end

  slot :empty_state,
    doc: "A message to show when the table is empty, to be used together with :col" do
    attr :row_class, :any
  end

  attr :rest, :global, include: ~w(colspan rowspan)

  def table(assigns) do
    assigns =
      with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
        assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
      end

    assigns = assign_new(assigns, :id, fn -> "table_#{Ecto.UUID.generate()}" end)

    ~H"""
    <table class={["pc-table--#{@variant}", @class]} {@rest}>
      <%= if @col != [] do %>
        <thead>
          <.tr>
            <.th :for={col <- @col} class={col[:class]}>{col[:label]}</.th>
          </.tr>
        </thead>
        <tbody id={@id} phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}>
          <.tr :if={@empty_state != []} id={@id <> "-empty"} class="hidden only:table-row">
            <.td
              :for={empty_state <- @empty_state}
              colspan={length(@col)}
              class={empty_state[:row_class]}
            >
              {render_slot(empty_state)}
            </.td>
          </.tr>
          <.tr
            :for={row <- @rows}
            id={@row_id && @row_id.(row)}
            class={["group", @row_click && "pc-table__tr--row-click"]}
          >
            <.td
              :for={{col, i} <- Enum.with_index(@col)}
              phx-click={@row_click && @row_click.(row)}
              class={[
                @row_click && "pc-table__td--row-click",
                i == 0 && "pc-table__td--first-col",
                col[:row_class] && col[:row_class]
              ]}
            >
              {render_slot(col, @row_item.(row))}
            </.td>
          </.tr>
        </tbody>
      <% else %>
        {render_slot(@inner_block)}
      <% end %>
    </table>
    """
  end

  attr(:class, :any, default: nil, doc: "CSS class")
  attr(:rest, :global, include: ~w(colspan rowspan))
  slot(:inner_block, required: false)

  def th(assigns) do
    ~H"""
    <th class={["pc-table__th", @class]} {@rest}>
      {render_slot(@inner_block)}
    </th>
    """
  end

  attr(:class, :any, default: nil, doc: "CSS class")
  attr(:rest, :global)
  slot(:inner_block, required: false)

  def tr(assigns) do
    ~H"""
    <tr class={["pc-table__tr", @class]} {@rest}>
      {render_slot(@inner_block)}
    </tr>
    """
  end

  attr(:class, :any, default: nil, doc: "CSS class")
  attr(:rest, :global, include: ~w(colspan headers rowspan))
  slot(:inner_block, required: false)

  def td(assigns) do
    ~H"""
    <td class={["pc-table__td", @class]} {@rest}>
      {render_slot(@inner_block)}
    </td>
    """
  end

  attr(:class, :any, default: nil, doc: "CSS class")
  attr(:label, :string, default: nil, doc: "Adds a label your user, e.g name")
  attr(:sub_label, :string, default: nil, doc: "Adds a sub-label your to your user, e.g title")
  attr(:rest, :global)

  attr(:avatar_assigns, :map,
    default: nil,
    doc: "if using an avatar, this map will be passed to the avatar component as props"
  )

  def user_inner_td(assigns) do
    ~H"""
    <div class={@class} {@rest}>
      <div class="pc-table__user-inner-td">
        <%= if @avatar_assigns do %>
          <.avatar {@avatar_assigns} />
        <% end %>

        <div class="pc-table__user-inner-td__inner">
          <div class="pc-table__user-inner-td__label">
            {@label}
          </div>
          <div class="pc-table__user-inner-td__sub-label">
            {@sub_label}
          </div>
        </div>
      </div>
    </div>
    """
  end
end