Packages
surface
0.8.2
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/catalogue/playground.ex
defmodule Surface.Catalogue.Playground do
@moduledoc """
A generic LiveView to create a component's playground for the Surface Catalogue.
## Options
Besides the buit-in options provided by the LiveView itself, a Playground also
provides the following options:
* `subject` - Required. The target component of the Playground.
* `height` - Required. The initial height of the Playground.
* `catalogue` - Optional. A module that implements the `Surface.Catalogue`
providing additional information to the catalogue tool. Usually required
if you want to share your components as a library.
* `body` - Optional. Sets/overrides the attributes of the Playground's body tag.
Useful to set a different background or padding.
## Initializing props and slots
If you need to define initial values for props or slots of the component, you can use
the `@props` and `@slots` module attributes, respectively. Both attributes are optional
and take a keyword list containing all the props/slots you need to initialize.
Pay attention that if you have required props/slots, you should provide initial values
for them.
## Example
defmodule MyApp.Components.MyButton.Playground do
use Surface.Catalogue.Playground,
subject: MyApp.Components.MyButton,
height: "170px"
@props [
class: "btn"
]
@slots [
default: "Cancel"
]
end
"""
import Phoenix.LiveView
@pubsub Surface.Catalogue.PubSub
defmacro __using__(opts) do
subject = Surface.Catalogue.fetch_subject!(opts, __MODULE__, __CALLER__)
quote do
@config unquote(opts)
@before_compile unquote(__MODULE__)
use Surface.LiveView, unquote(opts)
alias unquote(subject)
require Surface.Catalogue.Data, as: Data
@impl true
def mount(params, session, socket) do
unquote(__MODULE__).__mount__(params, session, socket, unquote(subject))
end
@impl true
def handle_info(message, socket) do
unquote(__MODULE__).__handle_info__(message, socket)
end
@impl true
def render(var!(assigns)) do
unquote(__MODULE__).inject_render()
end
defoverridable(render: 1)
end
end
# Retrieves or creates the window id that should be used to filter
# PubSub messages from the Playground.
@doc false
def get_window_id(session, params) do
key = "__window_id__"
get_value_by_key(session, key) ||
get_value_by_key(params, key) ||
Base.encode16(:crypto.strong_rand_bytes(16))
end
# Subscribes to receive notification messages from the Playground.
@doc false
def subscribe(window_id) do
if running_pubsub?() do
Phoenix.PubSub.subscribe(@pubsub, topic(window_id))
end
end
defp notify_init(window_id, subject, props, slots, events, assigns_values) do
if running_pubsub?() do
message = {:playground_init, self(), subject, props, slots, events, assigns_values}
Phoenix.PubSub.broadcast(@pubsub, topic(window_id), message)
end
end
defp notify_event_received(window_id, event, value, props) do
if running_pubsub?() do
message = {:playground_event_received, event, value, props}
Phoenix.PubSub.broadcast(@pubsub, topic(window_id), message)
end
end
defp topic(window_id) do
"#{@pubsub}:#{window_id}"
end
defmacro __before_compile__(env) do
config = Module.get_attribute(env.module, :config)
subject = Keyword.fetch!(config, :subject)
props_data = Module.get_attribute(env.module, :props, [])
slots_data = Module.get_attribute(env.module, :slots, [])
existing_props_data =
env.module
|> Module.get_attribute(:assigns, [])
|> Enum.find(fn %{name: name} -> name == :props end)
# TODO: Remove this validation in v0.9 and inject the `data props` code directly in `common_ast`
props_data_ast =
case existing_props_data do
%{line: line} ->
message = """
using `data props, :map, default: %{...}` has been deprecated in favor of `@props [...]`.
Example:
@props [
label: "My label",
color: "red",
]
"""
Surface.IOHelper.warn(message, env, line)
_ ->
quote do
data props, :keyword, default: unquote(Macro.escape(props_data))
end
end
common_ast =
quote do
@moduledoc catalogue: [
type: :playground,
subject: unquote(subject),
config: unquote(config)
]
unquote(props_data_ast)
data slots, :keyword, default: unquote(slots_data)
end
if Module.defines?(env.module, {:handle_event, 3}) do
quote do
unquote(common_ast)
defoverridable handle_event: 3
@impl true
def handle_event(event, value, socket) do
result = super(event, value, socket)
socket =
case result do
{:noreply, socket} -> socket
{:reply, _map, socket} -> socket
end
unquote(__MODULE__).__handle_event__(event, value, socket)
result
end
end
else
quote do
unquote(common_ast)
@impl true
def handle_event(event, value, socket) do
unquote(__MODULE__).__handle_event__(event, value, socket)
end
end
end
end
@doc false
def __mount__(params, session, socket, subject) do
window_id = get_window_id(session, params)
socket = assign(socket, :__window_id__, window_id)
if connected?(socket) do
{events, props} =
subject.__props__()
|> Enum.split_with(fn prop -> prop.type == :event end)
slots = Enum.map(subject.__slots__(), &Map.put(&1, :type, :string))
events_props_values = generate_events_props(events)
props_values =
props
|> get_props_default_values()
|> Map.merge(Map.new(socket.assigns.props))
|> Map.merge(events_props_values)
slots_values =
slots
|> init_slots_values()
|> Map.merge(Map.new(socket.assigns.slots))
assigns_values = Map.merge(props_values, slots_values)
notify_init(window_id, subject, props, slots, events, assigns_values)
{:ok, assign(socket, subject: subject, props: props_values, slots: slots_values)}
else
{:ok, socket}
end
end
@doc false
def __handle_info__({:update_props, values}, socket) do
props_names = Enum.map(socket.assigns.subject.__props__(), & &1.name)
props_values = Map.take(values, props_names)
slots_values = Map.new(socket.assigns.subject.__slots__(), fn %{name: name} -> {name, values[name]} end)
{:noreply, assign(socket, props: props_values, slots: slots_values)}
end
def __handle_info__(:wake_up, socket) do
{:noreply, socket}
end
@doc false
def __handle_event__(event, value, socket) do
window_id = socket.assigns[:__window_id__]
notify_event_received(window_id, event, value, socket.assigns.props)
{:noreply, socket}
end
@doc false
defmacro inject_render() do
subject = Module.get_attribute(__CALLER__.module, :config)[:subject]
slots = Enum.map(subject.__slots__(), & &1.name)
render_code =
if slots == [] do
"<#{subject} :props={@props}/>"
else
slots_code =
Enum.map_join(slots, fn slot ->
"<:#{slot} __ignore__={is_nil(@slots[#{inspect(slot)}])}>{@slots[#{inspect(slot)}]}</:#{slot}>"
end)
"<#{subject} :props={@props}>#{slots_code}</#{subject}>"
end
render_code
|> Surface.Compiler.compile(__CALLER__.line, __CALLER__)
|> Surface.Compiler.to_live_struct()
end
defp get_value_by_key(map, key) when is_map(map) do
map[key]
end
defp get_value_by_key(_map, _key) do
nil
end
defp generate_events_props(events) do
for %{name: name} <- events, into: %{} do
{name, %{name: name, target: :live_view}}
end
end
defp init_slots_values(slots) do
for %{name: name} <- slots, into: %{} do
{name, nil}
end
end
defp get_props_default_values(props) do
for %{name: name, opts: opts} <- props,
Keyword.has_key?(opts, :default),
into: %{} do
{name, opts[:default]}
end
end
defp running_pubsub? do
pid = Process.whereis(@pubsub)
pid && Process.alive?(pid)
end
end