Last active
July 19, 2022 10:09
-
-
Save teamon/7283980d7bcfb5f59b9fb5ced2ffc628 to your computer and use it in GitHub Desktop.
LiveView Storybook
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Hello.Admin.CopyLink do | |
use Phoenix.Component | |
@doc """ | |
Render a button (styled as a link) that copies a value to clipboard when clicked. | |
Storybook: Basic | |
<.copy_link value="https://example.com" text="copy link" /> | |
""" | |
def copy_link(assigns) do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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