phx_component_helpers

Extensible Phoenix liveview components, without boilerplate.
Demonstration & code samples.

set_attributes & extend_class

This is the basic example showing how you can pass attributes to your components with required values and providing defaults. It also illustrates how extend_class makes it easy to adjust your component styling from the markup.

This is a badge Badge with a dot Badge with red dot
    
<.badge label="This is a badge"/>
<.badge label="Badge with a dot" dot={true} class="ml-4"/>
<.badge label="Badge with red dot" dot={true} class="ml-4" dot_class="!text* text-red-400"/>
    
  
    
defmodule PhxComponentHelpersDemoWeb.Components.Badge do
  use PhxComponentHelpersDemoWeb, :component
  import PhxComponentHelpers

  @class "inline-flex items-center px-3 py-0.5 rounded-full\
          text-sm font-medium bg-blue-100 text-blue-800"

  @dot_class "-ml-0.5 mr-1.5 h-2.5 w-2 text-blue-400"

  def badge(assigns) do
    assigns
    |> set_attributes([:label, dot: false], required: [:label])
    |> extend_class(@class, prefix_replace: false)
    |> extend_class(@dot_class, attribute: :dot_class, prefix_replace: false)
    |> render()
  end

  defp render(assigns) do
    ~H"""
    <span {@heex_class}>
      <%= if @dot do %>
        <svg {@heex_dot_class} fill="currentColor" viewBox="0 0 7 7">
          <circle cx="4" cy="4" r="3" />
        </svg>
      <% end %>
      <%= @label %>
    </span>
    """
  end
end
    
  

live_component & phx_attributes

phx_component_helpers are also working for live_components. It provides a useful feature to pass all phx-* attributes at once to your component markup.
In this sample, we also show how extend_class can be used with a function.

    
<.live_component module={Button} id="button-1" label="I do nothing"/>

<.live_component module={Button} id="button-2"
  color={:purple}
  label="I change my color"
  phx-click="change_color"
/>
    
  
    
defmodule PhxComponentHelpersDemoWeb.Components.Button do
  use PhxComponentHelpersDemoWeb, :live_component
  import PhxComponentHelpers

  def update(assigns, socket) do
    assigns =
      assigns
      |> set_phx_attributes()
      |> set_attributes([:label, color: :indigo], required: [:label])
      |> extend_class(&button_class/1, prefix_replace: false)

    {:ok, assign(socket, assigns)}
  end

  def render(assigns) do
    ~H"""
    <button {@heex_class} {@heex_phx_attributes} phx-target={@myself}>
      <%= @label %>
    </button>
    """
  end

  def handle_event("change_color", _, socket) do
    assigns = extend_class(%{color: random_color()}, &button_class/1, prefix_replace: false)
    {:noreply, assign(socket, assigns)}
  end

  defp button_class(assigns) do
    "inline-flex items-center px-4 py-2 border border-transparent text-sm\
    font-medium rounded-md shadow-sm text-white #{color(assigns)}"
  end

  defp random_color, do: Enum.random([:indigo, :yellow, :red, :purple, :emerald])
  defp color(%{color: :indigo}), do: "bg-indigo-600 hover:bg-indigo-700"
  defp color(%{color: :yellow}), do: "bg-yellow-400 hover:bg-yellow-500"
  defp color(%{color: :red}), do: "bg-red-600 hover:bg-red-700"
  defp color(%{color: :purple}), do: "bg-purple-600 hover:bg-purple-700"
  defp color(%{color: :emerald}), do: "bg-emerald-600 hover:bg-emerald-700"
end
    
  

set_prefixed_attributes

Using set_prefixed_attributes you can forward multiple assigns at once. For example it's very convenient to detect and merge a whole set of x- attributes when using alpinejs.

    
<.dropdown 
  label="I'm a dropdown"
  x-data="{open: false}"
  x-bind:class="open ? '' : 'hidden'"
  x-on:click="open = !open"
/>
    
  
    
defmodule PhxComponentHelpersDemoWeb.Components.Dropdown do
  use PhxComponentHelpersDemoWeb, :component
  import PhxComponentHelpers

  @class "relative inline-block text-left"
  @button_class "inline-flex justify-center w-full rounded-md border border-gray-300\
                shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50"
  @dropdown_class "origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white"
  @dropdown_entry_class "text-gray-700 block px-4 py-2 text-sm"

  def dropdown(assigns) do
    assigns
    |> set_attributes(label: "Dropdown")
    |> extend_class(@class, prefix_replace: false)
    |> extend_class(@button_class, attribute: :button_class, prefix_replace: false)
    |> extend_class(@dropdown_class, attribute: :dropdown_class, prefix_replace: false)
    |> extend_class(@dropdown_entry_class, attribute: :dropdown_entry_class, prefix_replace: false)
    |> set_prefixed_attributes(["x-data"], into: :init_alpine)
    |> set_prefixed_attributes(["x-bind", "x-on"])
    |> render()
  end

  defp render(assigns) do
    ~H"""
    <div {@heex_class} {@heex_init_alpine}>
      <div>
        <button type="button" {@heex_button_class} {assigns[:"heex_x-on:click"]}>
          <%= @label %>
        </button>
      </div>

      <div {@heex_dropdown_class} {assigns[:"heex_x-bind:class"]}>
        <div class="py-1">
          <a {@heex_dropdown_entry_class}>Account settings</a>
          <a {@heex_dropdown_entry_class}>Support</a>
          <a {@heex_dropdown_entry_class}>License</a>
        </div>
      </div>
    </div>
    """
  end
end
    
  

forward_assigns

This code sample shows how nested components than still be customized from template markup: by using forward_assigns all assigns prefixed by :button_ are forwarded to the button sub component.

Beware!

With a button color

    
<.alert title="Beware!" button_id="alert-btn-1"/>
<.alert title="With a button color" button_id="alert-btn-2" button_color={:yellow}/>
    
  
    
defmodule PhxComponentHelpersDemoWeb.Components.Alert do
  use PhxComponentHelpersDemoWeb, :component
  import PhxComponentHelpers

  alias PhxComponentHelpersDemoWeb.Components.Button

  def alert(assigns) do
    assigns
    |> set_attributes([:title, :message], required: [:title])
    |> extend_class("rounded-md bg-yellow-50 p-4 mb-4", prefix_replace: false)
    |> extend_class("text-sm font-medium text-yellow-800",
      attribute: :title_class,
      prefix_replace: false
    )
    |> render()
  end

  defp render(assigns) do
    ~H"""
    <div {@heex_class}>
      <div class="flex w-full justify-between">
      <h3 {@heex_title_class}>
        <i class="fa fa-exclamation-circle text-yellow-400 mr-2 fa-lg"/>
        <%= @title %>
      </h3>
        <.live_component module={Button} label="Click me!"
          {forward_assigns(assigns, prefix: :button)}
        />
      </div>
    </div>
    """
  end
end