Skip to content

Instantly share code, notes, and snippets.

@teamon
Last active July 19, 2022 10:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save teamon/7283980d7bcfb5f59b9fb5ced2ffc628 to your computer and use it in GitHub Desktop.
Save teamon/7283980d7bcfb5f59b9fb5ced2ffc628 to your computer and use it in GitHub Desktop.
LiveView Storybook
defmodule Hello.Admin.Details do
use Phoenix.Component
import UpsideWeb.Admin.Loading
@doc """
Details component
Storybook: Basic
<%
user = %{name: "John", age: 35}
%>
<.render>
<:title>My Title</:title>
<:prop label="Name"><%= user.name %></:prop>
<:prop label="Age"><%= user.age %></:prop>
</.render>
Storybook: Loading state
<.render loading={true}>
<:title>My Title</:title>
<:prop label="Name">John Doe</:prop>
</.render>
Storybook: With subtitle
<.render>
<:title>My Title</:title>
<:subtitle>My Subtitle</:subtitle>
<:prop label="Name">John Doe</:prop>
</.render>
"""
def render(assigns) do
defmodule Hello.Storybook do
use Phoenix.LiveView
@modules [
Hello.Admin.Combobox,
Hello.Admin.CopyLink,
Hello.Admin.Details
]
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:components, components(@modules))
|> assign(:events, [])}
end
@impl true
def handle_event(event, data, socket) do
events = Enum.take([{event, data} | socket.assigns.events], 5)
{:noreply, socket |> assign(:events, events)}
end
@impl true
def render(assigns) do
~H"""
<div class="p-10">
<h1 class="text-xl">LiveView Storybook</h1>
<div>
<h3>Recent events</h3>
<%= if Enum.empty?(@events) do %>
<span>No events yet</span>
<% else %>
<table>
<tr>
<th>Event</th>
<th>Data</th>
</tr>
<%= for {event, data} <- @events do %>
<tr>
<td><code><%= inspect(event) %></code></td>
<td><code><%= inspect(data) %></code></td>
</tr>
<% end %>
</table>
<% end %>
</div>
<%= for component <- @components do %>
<div class="mt-10">
<h2 class="font-mono"><%= component.mfa %></h2>
<p class="text-sm text-gray-700"><%= component.desc %></p>
<%= for example <- component.examples do %>
<fieldset class="relative border border-yellow-300 my-2">
<legend class="absolute text-xs uppercase bg-yellow-300 text-black-900 px-2">
Example: <%= example.label %>
</legend>
<div class="grid grid-cols-2">
<div class="text-sm p-5 pt-8 bg-gray-200">
<pre><%= example.code %></pre>
</div>
<div class="p-5 pt-8">
<%= render_code(component, example) %>
</div>
</div>
</fieldset>
<% end %>
</div>
<% end %>
</div>
"""
end
defp components(modules) do
for mod <- modules, component <- extract(mod), do: component
end
defp extract(mod) do
{:docs_v1, _annotation, _, _, _moduledoc, _, docs} = Code.fetch_docs(mod)
for doc <- docs, fun <- extract_from_doc(doc, mod), do: fun
end
defp extract_from_doc({{_, _name, _arity}, _annotation, _, :none, _}, _mod) do
[]
end
defp extract_from_doc({{_, name, arity}, _annotation, _, %{"en" => doc}, _}, mod) do
{desc, examples} =
doc
|> String.split(["\r\n", "\n"], trim: false)
|> Enum.reduce({[], []}, fn
"Storybook: " <> label, {desc, examples} ->
{desc, [{label, []} | examples]}
" " <> code, {desc, [{label, codes} | rest]} ->
{desc, [{label, [code | codes]} | rest]}
other, {desc, []} ->
{[other | desc], []}
_other, acc ->
acc
end)
desc = Enum.join(Enum.reverse(desc), "\n")
examples =
examples
|> Enum.map(fn {label, codes} ->
code =
codes
|> Enum.reverse()
|> Enum.join("\n")
%{
label: label,
code: code
}
end)
|> Enum.reverse()
[
%{
mfa: mfa(mod, name, arity),
mod: mod,
name: name,
arity: arity,
desc: desc,
examples: examples
}
]
end
defp mfa(m, f, a) do
"#{Enum.join(Module.split(m), ".")}.#{f}/#{a}"
end
defp render_code(component, example) do
options = [
engine: Phoenix.LiveView.HTMLEngine,
module: component.mod
]
ast =
quote do
import unquote(component.mod)
unquote(EEx.compile_string(example.code, options))
end
{code, _} = Code.eval_quoted(ast, assigns: %{})
code
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment