LiveProps -- Props and States for Phoenix LiveView

LiveProps is a library for managing properties and state within Phoenix LiveViews and LiveComponents. This is my first package with Elixir so any feedback is welcome.

Features

  • Declaratively define props and state, initialize default values, and compute derived values.
  • Supports required props
  • Supports automatic re-computation of computed props and state
  • Props automatically added to module documentation.

Here is a simple example:

defmodule ButtonComponent do
  use Phoenix.LiveComponent
  use LiveProps.LiveComponent

  prop :class, :string, default: "button"
  prop :text, :string, default: "Click me"
  prop :on_click, :string, default: "click_button"

  def render(assigns) do
    ~L"""
    <button class="<%= @class %>"
            phx-click="<%= @on_click %>">
      <%= @text %>
    </button>
    """
  end
end

In this example we define three props that will be automatically assigned default values, so you don’t have to define your own mount or update callbacks (but you still can if you want and the defaults will still be assigned, unless you overwrite them)

Additional examples in the docs.

Docs
Hex
Github

Thanks

8 Likes

Wow this is really cool, and not very intrusive.

How would it look like if I want to define my own mount/1 and update/2 functions?

Thanks. Defining your own callbacks is no problem. Two things to know are that, for a component:

  • states are assigned in mount/1 and props are assigned in update/2 (or preload/1 if it is defined).
  • any callbacks you defined will be executed after the injected callbacks (so defaults/computed values will already be assigned).

Lastly, if you don’t want any of this behavior in a particular component or LiveView, that is fine. You don’t have to use it everywhere.

In code:

defmodule ThermostatComponent do
  use Phoenix.LiveComponent
  use LiveProps.LiveComponent

  prop :user_id, :integer, required: true
  prop :temperature, :float, compute: :get_temperature

  state :mode, :atom, default: :verbose

  def render(assigns) do
    ~L"""
    <%= case @mode  do %>
      <% :verbose -> %>
        Current temperature: <%= @temperature %>

      <% _ -> %>
        <%= @temperature %>
    <% end %>
    <button phx-click="toggle-mode" phx-target="<%= @myself %>">Toggle mode</button>
    """
  end

  def mount(socket) do
    # socket.assigns has :mode == :verbose
    # but props are not assigned yet

    # do something
    {:ok, socket}
  end

  def update(assigns, socket) do
    # Assigns are the original assigns passed in to the component.
    # They have already been merged into the socket.assigns so you 
    # do not have to do it again.

    # Additionally, the :temperature prop has already been computed and assigned.

    # do something

    {:ok, socket}
  end

  def get_temperature(assigns) do
    Thermostat.get_user_reading(assigns.user_id)
  end

  def handle_event("toggle-mode", _, %{assigns: assigns} = socket) do
    new_mode = if assigns.mode == :verbose, do: :compact, else: :verbose

    {:noreply, assign(socket, :mode, new_mode)}
  end
end
2 Likes