defmodule Love.Component do
@moduledoc """
Extend LiveComponents.
## Usage
Add `use Love.Component` to a `Phoenix.LiveComponent`. This adds:
- `@behaviour Love.Events` for the optional `c:Love.Events.handle_message/4` callback
- `import Love.Component` to make macros and functions locally available
- `prop :id` to define the required `:id` prop assign
- `mount/1` and `update/2` default implementations (can safely overriden)
## Love.Component Example
defmodule MyAppWeb.UserProfileComponent do
use Phoenix.LiveComponent
use Love.Component
prop :profile
prop :show_avatar?, default: false
state :age
state :expand_details?, default: false
slot :inner_block
event :on_selected
def handle_event("toggle-details", _, socket) do
{:noreply, put_state(socket, socket, expand_details?: not socket.assigns.expand_details?)}
end
def handle_event("select", %{"profile_id" => profile_id}}, socket) do
{:noreply, emit(socket, :on_selected, profile_id)}
end
@react to: :profile
defp put_age(socket) do
age = trunc(Date.diff(Date.utc_today(), socket.assigns.profile.birthday) / 365)
put_state(socket, age: age)
end
end
"""
alias Love.Internal
alias Phoenix.LiveView
##################################################
# __using__/1
##################################################
defmacro __using__(_opts) do
Internal.init_module_attributes(__CALLER__, [:react, :prop, :state, :defaults])
quote do
@behaviour Love.Events
@on_definition {Love.Internal, :on_definition}
@before_compile Love.Component
import Love.Component
prop :id
end
end
##################################################
# __before_compile__/1
##################################################
defmacro __before_compile__(env) do
# Evaluate quoted opts and turn them into more useful structures
Internal.before_compile_eval_metas(env, [:prop, :state])
# Add the :triggers fields, so we know what to reevaluate when a field changes
Internal.before_compile_put_meta_triggers(env.module, [:prop, :state])
# Delay these function definitions until as late as possible, so we can ensure the attributes
# are fully set up (i.e. wait for __on_definition__/6 to evaluate first!)
[
Internal.before_compile_define_meta_fns(env, [:prop, :state, :react]),
Internal.define_defaults(env.module),
Internal.before_compile_define_react_wrappers(env),
wrap_mount(env),
wrap_update(env)
]
end
# Learned this technique here:
# https://github.com/surface-ui/surface/blob/a93cfa753cb5bb7155981f4328bb64d01fa5e579/lib/surface/live_view.ex#L77-L104
defp wrap_mount(env) do
if Module.defines?(env.module, {:mount, 1}) do
quote do
defoverridable mount: 1
def mount(socket) do
socket = Internal.component_mount_hook(socket, __MODULE__)
super(socket)
end
end
else
quote do
def mount(socket) do
{:ok, Internal.component_mount_hook(socket, __MODULE__)}
end
end
end
end
defp wrap_update(env) do
if Module.defines?(env.module, {:update, 2}) do
quote do
defoverridable update: 2
def update(assigns, socket) do
socket = Internal.component_update_hook(socket, assigns)
super(assigns, socket)
end
end
else
quote do
def update(assigns, socket) do
{:ok, Internal.component_update_hook(socket, assigns)}
end
end
end
end
##################################################
# PUBLIC API
##################################################
@doc """
Defines a prop.
`prop :id` is automatically defined as a required prop for all components that `use Love.Component`,
because every stateful `LiveComponent` requires an `:id`.
## Options
- `:default` - optional; if specified, this prop is considered optional, and will be assigned the default
value during mount. If not specified, the prop is considered required. `nil` is a valid default value. The
expression for the default value is wrapped in a function and its evaluation is deferred until runtime
at the moment the component is mounted.
## Example
# A required prop
prop :visible?
# An optional prop
prop :thumbnail_url, default: nil
"""
@doc group: :fields
@spec prop(key :: atom, opts :: keyword) :: nil
defmacro prop(key, opts \\ []) when is_atom(key) do
Internal.define_prop(__CALLER__, key, opts)
end
@doc """
Defines a slot prop.
## Options
- `:required?` - defaults to `true`. When `false`, the prop given the empty slot value of `[]`
## Example
# Default slot name
slot :inner_block
# Optional slot
slot :navbar, required?: false
"""
@doc group: :fields
@spec slot(key :: atom, opts :: keyword) :: nil
defmacro slot(key, opts \\ []) when is_atom(key) do
prop_opts =
if opts[:required?] == false do
[default: []]
else
[]
end
Internal.define_prop(__CALLER__, key, prop_opts)
end
@doc """
Defines an event prop.
Event props are always optional, and default to `nil`.
The value of this prop must be a destination to receive the event, either a PID or `{module, id}`.
See `emit/3` for details on raising events.
The emitted event name defaults to the name of the event prop. The event name can be overridden
by the parent specifying `{pid, :my_custom_event_name}` or `{module, id, :my_custom_event_name}`.
## Example
event :on_selected
# To raise it:
emit(socket, :on_selected, "some payload")
# To handle it:
handle_event(:on_selected, {module, id}, "some payload", socket)
"""
@doc group: :fields
@spec event(name :: atom) :: nil
defmacro event(name) when is_atom(name) do
Internal.define_prop(__CALLER__, name, default: nil)
end
@doc """
Defines a state assign.
State is internal to the component and is modified via `put_state/2`.
## Options
- `:default` - optional; if specified, the state will be assigned the default value during mount.
The expression for the default value is wrapped in a function and its evaluation is deferred until runtime
at the moment the component is mounted. If not specified, you should `put_state/2` during component
initialization to set an initial value.
## Example
# State with no initial value
state :changeset
# State with an initial value, evaluated during mount
state :now, default: DateTime.utc_now()
"""
@doc group: :fields
@spec state(key :: atom, opts :: keyword) :: nil
defmacro state(key, opts \\ []) when is_atom(key) do
Internal.define_state(__CALLER__, key, opts)
end
@doc """
Updates state assigns.
When called outside of a reactive function, any reactive functions that depend on the changed
state will be immediately evaluated, so call this function as infrequently as possible. In
other words, try to batch state changes and limit `put_state/2` calls to once per lifecycle event.
Within a reactive function, any additionally-triggered reactive functions will
be deferred until after the current reactive function completely executes.
Returns the socket with the new state and after any reactive callbacks have run.
## Example
state :first_name
put_state(socket, first_name: "Marvin")
"""
@spec put_state(LiveView.Socket.t(), map | keyword) :: LiveView.Socket.t()
def put_state(socket, changes) do
Internal.put_state(socket, changes)
end
@doc """
Sends an event message.
The event `name` is an event prop defined by `event/1`. The destination for the event is determined
by the value of the event prop. See `Love.Events.send_message/4` for details on valid destinations.
"""
@spec emit(LiveView.Socket.t(), name :: atom, payload :: any) :: LiveView.Socket.t()
defdelegate emit(socket, name, payload \\ nil), to: Internal
end