Slots!
Integration test for slots Format integration test project Hide slots assign in temple prefixed key Won't compile temple related assigns when calling Utils.runtime_attrs Update component docs with slots usage
This commit is contained in:
parent
f7197ede4a
commit
851f6415fe
26 changed files with 457 additions and 80 deletions
|
@ -1,5 +1,5 @@
|
||||||
locals_without_parens = ~w[
|
locals_without_parens = ~w[
|
||||||
temple c
|
temple c slot
|
||||||
html head title style script
|
html head title style script
|
||||||
noscript template
|
noscript template
|
||||||
body section nav article aside h1 h2 h3 h4 h5 h6
|
body section nav article aside h1 h2 h3 h4 h5 h6
|
||||||
|
|
|
@ -191,3 +191,8 @@ To include Temple's formatter configuration, add `:temple` to your `.formatter.e
|
||||||
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs,lexs}"],
|
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs,lexs}"],
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Introducing Temple: An elegant HTML library for Elixir and Phoenix](https://www.mitchellhanberg.com/introducing-temple-an-elegant-html-library-for-elixir-and-phoenix/)
|
||||||
|
- [Temple, AST, and Protocols](https://www.mitchellhanberg.com/temple-ast-and-protocols/)
|
||||||
|
|
|
@ -29,6 +29,5 @@ config :wallaby,
|
||||||
otp_app: :temple_demo,
|
otp_app: :temple_demo,
|
||||||
screenshot_on_failure: true
|
screenshot_on_failure: true
|
||||||
|
|
||||||
|
|
||||||
# Print only warnings and errors during test
|
# Print only warnings and errors during test
|
||||||
config :logger, level: :warn
|
config :logger, level: :warn
|
||||||
|
|
|
@ -38,6 +38,7 @@ defmodule TempleDemoWeb do
|
||||||
|
|
||||||
alias TempleDemoWeb.Component.Outer
|
alias TempleDemoWeb.Component.Outer
|
||||||
alias TempleDemoWeb.Component.Flash
|
alias TempleDemoWeb.Component.Flash
|
||||||
|
alias TempleDemoWeb.Component.Form
|
||||||
|
|
||||||
# Include shared imports and aliases for views
|
# Include shared imports and aliases for views
|
||||||
unquote(view_helpers())
|
unquote(view_helpers())
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
defmodule TempleDemoWeb.Component.Form do
|
||||||
|
use Temple.Component
|
||||||
|
|
||||||
|
render do
|
||||||
|
f = Phoenix.HTML.Form.form_for(@changeset, @action)
|
||||||
|
|
||||||
|
f
|
||||||
|
|
||||||
|
slot(:f, f: f)
|
||||||
|
|
||||||
|
"</form>"
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,6 @@ defmodule TempleDemoWeb.Endpoint do
|
||||||
plug Phoenix.Ecto.SQL.Sandbox
|
plug Phoenix.Ecto.SQL.Sandbox
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# The session will be stored in the cookie and signed,
|
# The session will be stored in the cookie and signed,
|
||||||
# this means its contents can be read but not tampered with.
|
# this means its contents can be read but not tampered with.
|
||||||
# Set :encryption_salt if you would also like to encrypt it.
|
# Set :encryption_salt if you would also like to encrypt it.
|
||||||
|
|
|
@ -12,6 +12,7 @@ section class: "phx-hero" do
|
||||||
p do
|
p do
|
||||||
"Peace-of-mind from prototype to staging"
|
"Peace-of-mind from prototype to staging"
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
p do
|
p do
|
||||||
"Peace-of-mind from prototype to production"
|
"Peace-of-mind from prototype to production"
|
||||||
|
|
|
@ -1,27 +1,29 @@
|
||||||
form_for @changeset, @action, fn f ->
|
c Form, changeset: @changeset, action: @action do
|
||||||
if @changeset.action do
|
slot :f, %{f: f} do
|
||||||
c Flash, type: :info do
|
if @changeset.action do
|
||||||
p do: "Oops, something went wrong! Please check the errors below."
|
c Flash, type: :info do
|
||||||
|
p do: "Oops, something went wrong! Please check the errors below."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
label f, :title
|
||||||
|
text_input f, :title
|
||||||
|
error_tag(f, :title)
|
||||||
|
|
||||||
|
label f, :body
|
||||||
|
textarea f, :body
|
||||||
|
error_tag(f, :body)
|
||||||
|
|
||||||
|
label f, :published_at
|
||||||
|
datetime_select f, :published_at
|
||||||
|
error_tag(f, :published_at)
|
||||||
|
|
||||||
|
label f, :author
|
||||||
|
text_input f, :author
|
||||||
|
error_tag(f, :author)
|
||||||
|
|
||||||
|
div do
|
||||||
|
submit "Save"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
label f, :title
|
|
||||||
text_input f, :title
|
|
||||||
error_tag(f, :title)
|
|
||||||
|
|
||||||
label f, :body
|
|
||||||
textarea f, :body
|
|
||||||
error_tag(f, :body)
|
|
||||||
|
|
||||||
label f, :published_at
|
|
||||||
datetime_select f, :published_at
|
|
||||||
error_tag(f, :published_at)
|
|
||||||
|
|
||||||
label f, :author
|
|
||||||
text_input f, :author
|
|
||||||
error_tag(f, :author)
|
|
||||||
|
|
||||||
div do
|
|
||||||
submit "Save"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,16 +11,20 @@ table do
|
||||||
|
|
||||||
tbody do
|
tbody do
|
||||||
for post <- @posts do
|
for post <- @posts do
|
||||||
tr do
|
tr do
|
||||||
td do: post.title
|
td do: post.title
|
||||||
td do: post.body
|
td do: post.body
|
||||||
td do: post.published_at
|
td do: post.published_at
|
||||||
td do: post.author
|
td do: post.author
|
||||||
|
|
||||||
td do
|
td do
|
||||||
link "Show", to: Routes.post_path(@conn, :show, post)
|
link "Show", to: Routes.post_path(@conn, :show, post)
|
||||||
link "Edit", to: Routes.post_path(@conn, :edit, post)
|
link "Edit", to: Routes.post_path(@conn, :edit, post)
|
||||||
link "Delete", to: Routes.post_path(@conn, :delete, post),
|
|
||||||
method: :delete, data: [confirm: "Are you sure?"]
|
link "Delete",
|
||||||
|
to: Routes.post_path(@conn, :delete, post),
|
||||||
|
method: :delete,
|
||||||
|
data: [confirm: "Are you sure?"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
h1 do: "Show Post"
|
h1 do: "Show Post"
|
||||||
|
|
||||||
ul do
|
ul do
|
||||||
li do: [strong(do: "Title"), @post.title]
|
li do: [strong(do: "Title"), @post.title]
|
||||||
|
|
||||||
li do
|
li do
|
||||||
strong do: "Body"
|
strong do: "Body"
|
||||||
Phoenix.HTML.Format.text_to_html @post.body, attributes: [class: "whitespace-pre"]
|
Phoenix.HTML.Format.text_to_html(@post.body, attributes: [class: "whitespace-pre"])
|
||||||
end
|
end
|
||||||
|
|
||||||
li do
|
li do
|
||||||
strong do: "Published at"
|
strong do: "Published at"
|
||||||
@post.published_at
|
@post.published_at
|
||||||
end
|
end
|
||||||
|
|
||||||
li do
|
li do
|
||||||
strong do: "Author"
|
strong do: "Author"
|
||||||
@post.author
|
@post.author
|
||||||
end
|
end
|
||||||
|
|
||||||
span do
|
span do
|
||||||
link "Edit", to: Routes.post_path(@conn, :edit, @post)
|
link "Edit", to: Routes.post_path(@conn, :edit, @post)
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule TempleDemoWeb.PostView do
|
||||||
|
|
||||||
defcomp Headers do
|
defcomp Headers do
|
||||||
thead id: PostView.thing() do
|
thead id: PostView.thing() do
|
||||||
tr do
|
tr do
|
||||||
@inner_content
|
@inner_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,5 @@ defmodule TempleDemo.Repo.Migrations.CreatePosts do
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,18 @@ defmodule TempleDemo.BlogTest do
|
||||||
describe "posts" do
|
describe "posts" do
|
||||||
alias TempleDemo.Blog.Post
|
alias TempleDemo.Blog.Post
|
||||||
|
|
||||||
@valid_attrs %{author: "some author", body: "some body", published_at: ~N[2010-04-17 14:00:00], title: "some title"}
|
@valid_attrs %{
|
||||||
@update_attrs %{author: "some updated author", body: "some updated body", published_at: ~N[2011-05-18 15:01:01], title: "some updated title"}
|
author: "some author",
|
||||||
|
body: "some body",
|
||||||
|
published_at: ~N[2010-04-17 14:00:00],
|
||||||
|
title: "some title"
|
||||||
|
}
|
||||||
|
@update_attrs %{
|
||||||
|
author: "some updated author",
|
||||||
|
body: "some updated body",
|
||||||
|
published_at: ~N[2011-05-18 15:01:01],
|
||||||
|
title: "some updated title"
|
||||||
|
}
|
||||||
@invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
|
@invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
|
||||||
|
|
||||||
def post_fixture(attrs \\ %{}) do
|
def post_fixture(attrs \\ %{}) do
|
||||||
|
|
|
@ -3,8 +3,18 @@ defmodule TempleDemoWeb.PostControllerTest do
|
||||||
|
|
||||||
alias TempleDemo.Blog
|
alias TempleDemo.Blog
|
||||||
|
|
||||||
@create_attrs %{author: "some author", body: "some body", published_at: ~N[2010-04-17 14:00:00], title: "some title"}
|
@create_attrs %{
|
||||||
@update_attrs %{author: "some updated author", body: "some updated body", published_at: ~N[2011-05-18 15:01:01], title: "some updated title"}
|
author: "some author",
|
||||||
|
body: "some body",
|
||||||
|
published_at: ~N[2010-04-17 14:00:00],
|
||||||
|
title: "some title"
|
||||||
|
}
|
||||||
|
@update_attrs %{
|
||||||
|
author: "some updated author",
|
||||||
|
body: "some updated body",
|
||||||
|
published_at: ~N[2011-05-18 15:01:01],
|
||||||
|
title: "some updated title"
|
||||||
|
}
|
||||||
@invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
|
@invalid_attrs %{author: nil, body: nil, published_at: nil, title: nil}
|
||||||
|
|
||||||
def fixture(:post) do
|
def fixture(:post) do
|
||||||
|
@ -75,6 +85,7 @@ defmodule TempleDemoWeb.PostControllerTest do
|
||||||
test "deletes chosen post", %{conn: conn, post: post} do
|
test "deletes chosen post", %{conn: conn, post: post} do
|
||||||
conn = delete(conn, Routes.post_path(conn, :delete, post))
|
conn = delete(conn, Routes.post_path(conn, :delete, post))
|
||||||
assert redirected_to(conn) == Routes.post_path(conn, :index)
|
assert redirected_to(conn) == Routes.post_path(conn, :index)
|
||||||
|
|
||||||
assert_error_sent 404, fn ->
|
assert_error_sent 404, fn ->
|
||||||
get(conn, Routes.post_path(conn, :show, post))
|
get(conn, Routes.post_path(conn, :show, post))
|
||||||
end
|
end
|
||||||
|
|
|
@ -56,6 +56,46 @@ defmodule Temple.Component do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
Components can use slots, which are named placeholders that can be called like functions to be able to pass them data. This is very useful
|
||||||
|
when a component needs to pass data from the inside of the component back to the caller, like when rendering a form in LiveView.
|
||||||
|
|
||||||
|
The definition of a slot happens at the call site of the component and you utilize that slot from inside of the component module.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Form do
|
||||||
|
use Temple.Component
|
||||||
|
|
||||||
|
render do
|
||||||
|
form = form_for(@changeset, @action, assigns)
|
||||||
|
|
||||||
|
form
|
||||||
|
|
||||||
|
slot(:f, form: form)
|
||||||
|
|
||||||
|
"</form>"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# lib/my_app_web/templates/post/new.html.lexs
|
||||||
|
|
||||||
|
c Form, changeset: @changeset,
|
||||||
|
action: @action,
|
||||||
|
class: "form-control",
|
||||||
|
phx_submit: :save,
|
||||||
|
phx_change: :validate do
|
||||||
|
slot :f, %{form: f} do
|
||||||
|
label f do
|
||||||
|
"Widget Name"
|
||||||
|
text_input f, :name, class: "text-input"
|
||||||
|
end
|
||||||
|
|
||||||
|
submit "Save!"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defmacro __using__(_) do
|
defmacro __using__(_) do
|
||||||
|
|
|
@ -6,6 +6,7 @@ defmodule Temple.Parser do
|
||||||
alias Temple.Parser.TempleNamespaceNonvoid
|
alias Temple.Parser.TempleNamespaceNonvoid
|
||||||
alias Temple.Parser.TempleNamespaceVoid
|
alias Temple.Parser.TempleNamespaceVoid
|
||||||
alias Temple.Parser.Components
|
alias Temple.Parser.Components
|
||||||
|
alias Temple.Parser.Slot
|
||||||
alias Temple.Parser.NonvoidElementsAliases
|
alias Temple.Parser.NonvoidElementsAliases
|
||||||
alias Temple.Parser.VoidElementsAliases
|
alias Temple.Parser.VoidElementsAliases
|
||||||
alias Temple.Parser.AnonymousFunctions
|
alias Temple.Parser.AnonymousFunctions
|
||||||
|
@ -18,6 +19,7 @@ defmodule Temple.Parser do
|
||||||
%Empty{}
|
%Empty{}
|
||||||
| %Text{}
|
| %Text{}
|
||||||
| %Components{}
|
| %Components{}
|
||||||
|
| %Slot{}
|
||||||
| %NonvoidElementsAliases{}
|
| %NonvoidElementsAliases{}
|
||||||
| %VoidElementsAliases{}
|
| %VoidElementsAliases{}
|
||||||
| %AnonymousFunctions{}
|
| %AnonymousFunctions{}
|
||||||
|
@ -83,21 +85,23 @@ defmodule Temple.Parser do
|
||||||
def void_elements_aliases, do: @void_elements_aliases
|
def void_elements_aliases, do: @void_elements_aliases
|
||||||
def void_elements_lookup, do: @void_elements_lookup
|
def void_elements_lookup, do: @void_elements_lookup
|
||||||
|
|
||||||
def parsers(),
|
def parsers() do
|
||||||
do: [
|
[
|
||||||
Temple.Parser.Empty,
|
Empty,
|
||||||
Temple.Parser.Text,
|
Text,
|
||||||
Temple.Parser.TempleNamespaceNonvoid,
|
TempleNamespaceNonvoid,
|
||||||
Temple.Parser.TempleNamespaceVoid,
|
TempleNamespaceVoid,
|
||||||
Temple.Parser.Components,
|
Components,
|
||||||
Temple.Parser.NonvoidElementsAliases,
|
Slot,
|
||||||
Temple.Parser.VoidElementsAliases,
|
NonvoidElementsAliases,
|
||||||
Temple.Parser.AnonymousFunctions,
|
VoidElementsAliases,
|
||||||
Temple.Parser.RightArrow,
|
AnonymousFunctions,
|
||||||
Temple.Parser.DoExpressions,
|
RightArrow,
|
||||||
Temple.Parser.Match,
|
DoExpressions,
|
||||||
Temple.Parser.Default
|
Match,
|
||||||
|
Default
|
||||||
]
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def parse({:__block__, _, asts}) do
|
def parse({:__block__, _, asts}) do
|
||||||
parse(asts)
|
parse(asts)
|
||||||
|
@ -113,6 +117,7 @@ defmodule Temple.Parser do
|
||||||
{_, false} <- {TempleNamespaceNonvoid, TempleNamespaceNonvoid.applicable?(ast)},
|
{_, false} <- {TempleNamespaceNonvoid, TempleNamespaceNonvoid.applicable?(ast)},
|
||||||
{_, false} <- {TempleNamespaceVoid, TempleNamespaceVoid.applicable?(ast)},
|
{_, false} <- {TempleNamespaceVoid, TempleNamespaceVoid.applicable?(ast)},
|
||||||
{_, false} <- {Components, Components.applicable?(ast)},
|
{_, false} <- {Components, Components.applicable?(ast)},
|
||||||
|
{_, false} <- {Slot, Slot.applicable?(ast)},
|
||||||
{_, false} <- {NonvoidElementsAliases, NonvoidElementsAliases.applicable?(ast)},
|
{_, false} <- {NonvoidElementsAliases, NonvoidElementsAliases.applicable?(ast)},
|
||||||
{_, false} <- {VoidElementsAliases, VoidElementsAliases.applicable?(ast)},
|
{_, false} <- {VoidElementsAliases, VoidElementsAliases.applicable?(ast)},
|
||||||
{_, false} <- {AnonymousFunctions, AnonymousFunctions.applicable?(ast)},
|
{_, false} <- {AnonymousFunctions, AnonymousFunctions.applicable?(ast)},
|
||||||
|
|
|
@ -2,7 +2,7 @@ defmodule Temple.Parser.Components do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
@behaviour Temple.Parser
|
@behaviour Temple.Parser
|
||||||
|
|
||||||
defstruct module: nil, assigns: [], children: []
|
defstruct module: nil, assigns: [], children: [], slots: []
|
||||||
|
|
||||||
@impl Temple.Parser
|
@impl Temple.Parser
|
||||||
def applicable?({:c, _, _}) do
|
def applicable?({:c, _, _}) do
|
||||||
|
@ -19,22 +19,68 @@ defmodule Temple.Parser.Components do
|
||||||
|
|
||||||
{do_and_else, assigns} = Temple.Parser.Utils.consolidate_blocks(do_and_else, args)
|
{do_and_else, assigns} = Temple.Parser.Utils.consolidate_blocks(do_and_else, args)
|
||||||
|
|
||||||
|
{default_slot, named_slots} =
|
||||||
|
if children = do_and_else[:do] do
|
||||||
|
Macro.postwalk(
|
||||||
|
children,
|
||||||
|
%{},
|
||||||
|
fn
|
||||||
|
{:slot, _, [name | args]}, named_slots ->
|
||||||
|
{assigns, slot} = split_assigns_and_children(args, Macro.escape(%{}))
|
||||||
|
|
||||||
|
{nil, Map.put(named_slots, name, %{assigns: assigns, slot: slot})}
|
||||||
|
|
||||||
|
node, named_slots ->
|
||||||
|
{node, named_slots}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
else
|
||||||
|
{nil, %{}}
|
||||||
|
end
|
||||||
|
|
||||||
children =
|
children =
|
||||||
if do_and_else[:do] == nil do
|
if default_slot == nil do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
Temple.Parser.parse(do_and_else[:do])
|
Temple.Parser.parse(default_slot)
|
||||||
|
end
|
||||||
|
|
||||||
|
slots =
|
||||||
|
for {name, %{slot: slot, assigns: assigns}} <- named_slots do
|
||||||
|
Temple.Ast.new(
|
||||||
|
Temple.Parser.Slottable,
|
||||||
|
name: name,
|
||||||
|
content: Temple.Parser.parse(slot),
|
||||||
|
assigns: assigns
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
Temple.Ast.new(__MODULE__,
|
Temple.Ast.new(__MODULE__,
|
||||||
module: Macro.expand_once(component_module, __ENV__),
|
module: Macro.expand_once(component_module, __ENV__),
|
||||||
assigns: assigns,
|
assigns: assigns,
|
||||||
|
slots: slots,
|
||||||
children: children
|
children: children
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp split_assigns_and_children(args, empty) do
|
||||||
|
case args do
|
||||||
|
[assigns, [do: block]] ->
|
||||||
|
{assigns, block}
|
||||||
|
|
||||||
|
[[do: block]] ->
|
||||||
|
{empty, block}
|
||||||
|
|
||||||
|
[assigns] ->
|
||||||
|
{assigns, nil}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{empty, nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defimpl Temple.Generator do
|
defimpl Temple.Generator do
|
||||||
def to_eex(%{module: module, assigns: assigns, children: []}) do
|
def to_eex(%{module: module, assigns: assigns, children: [], slots: slots}) do
|
||||||
[
|
[
|
||||||
"<%= Phoenix.View.render",
|
"<%= Phoenix.View.render",
|
||||||
" ",
|
" ",
|
||||||
|
@ -42,22 +88,45 @@ defmodule Temple.Parser.Components do
|
||||||
", ",
|
", ",
|
||||||
":self,",
|
":self,",
|
||||||
" ",
|
" ",
|
||||||
|
"[{:__temple_slots__, %{",
|
||||||
|
for slot <- slots do
|
||||||
|
[
|
||||||
|
to_string(slot.name),
|
||||||
|
": ",
|
||||||
|
"fn #{Macro.to_string(slot.assigns)} -> %>",
|
||||||
|
for(child <- slot.content, do: Temple.Generator.to_eex(child)),
|
||||||
|
"<% end, "
|
||||||
|
]
|
||||||
|
end,
|
||||||
|
"}} | ",
|
||||||
Macro.to_string(assigns),
|
Macro.to_string(assigns),
|
||||||
|
"]",
|
||||||
" ",
|
" ",
|
||||||
"%>"
|
"%>"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_eex(%{module: module, assigns: assigns, children: children}) do
|
def to_eex(%{module: module, assigns: assigns, children: children, slots: slots}) do
|
||||||
[
|
[
|
||||||
"<%= Phoenix.View.render_layout ",
|
"<%= Phoenix.View.render_layout ",
|
||||||
Macro.to_string(module),
|
Macro.to_string(module),
|
||||||
", ",
|
", ",
|
||||||
":self",
|
":self,",
|
||||||
", ",
|
|
||||||
Macro.to_string(assigns),
|
|
||||||
" ",
|
" ",
|
||||||
"do %>",
|
"[{:__temple_slots__, %{",
|
||||||
|
for slot <- slots do
|
||||||
|
[
|
||||||
|
to_string(slot.name),
|
||||||
|
": ",
|
||||||
|
"fn #{Macro.to_string(slot.assigns)} -> %>",
|
||||||
|
for(child <- slot.content, do: Temple.Generator.to_eex(child)),
|
||||||
|
"<% end, "
|
||||||
|
]
|
||||||
|
end,
|
||||||
|
"}} | ",
|
||||||
|
Macro.to_string(assigns),
|
||||||
|
"]",
|
||||||
|
" do %>",
|
||||||
"\n",
|
"\n",
|
||||||
for(child <- children, do: Temple.Generator.to_eex(child)),
|
for(child <- children, do: Temple.Generator.to_eex(child)),
|
||||||
"\n",
|
"\n",
|
||||||
|
|
30
lib/temple/parser/slot.ex
Normal file
30
lib/temple/parser/slot.ex
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
defmodule Temple.Parser.Slot do
|
||||||
|
@moduledoc false
|
||||||
|
@behaviour Temple.Parser
|
||||||
|
|
||||||
|
defstruct name: nil, args: []
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def applicable?({:slot, _, _}) do
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def applicable?(_), do: false
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def run({:slot, _, [slot_name | [args]]}) do
|
||||||
|
Temple.Ast.new(__MODULE__, name: slot_name, args: args)
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Temple.Generator do
|
||||||
|
def to_eex(%{name: name, args: args}) do
|
||||||
|
[
|
||||||
|
"<%= @__temple_slots__.",
|
||||||
|
to_string(name),
|
||||||
|
".(",
|
||||||
|
Macro.to_string(quote(do: Enum.into(unquote(args), %{}))),
|
||||||
|
") %>"
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
3
lib/temple/parser/slottable.ex
Normal file
3
lib/temple/parser/slottable.ex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule Temple.Parser.Slottable do
|
||||||
|
defstruct content: nil, assigns: Macro.escape(%{}), name: nil
|
||||||
|
end
|
|
@ -34,7 +34,7 @@ defmodule Temple.Parser.Utils do
|
||||||
|
|
||||||
def runtime_attrs(attrs) do
|
def runtime_attrs(attrs) do
|
||||||
{:safe,
|
{:safe,
|
||||||
for {name, value} <- attrs, into: "" do
|
for {name, value} <- attrs, name != :__temple_slots__, into: "" do
|
||||||
name = snake_to_kebab(name)
|
name = snake_to_kebab(name)
|
||||||
|
|
||||||
" " <> name <> "=\"" <> to_string(value) <> "\""
|
" " <> name <> "=\"" <> to_string(value) <> "\""
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Temple.ComponentTest do
|
||||||
use Temple
|
use Temple
|
||||||
use Temple.Support.Utils
|
use Temple.Support.Utils
|
||||||
|
|
||||||
# `Phoenix.View.render_layout/4` is a phoenix function used for rendering partials that contain inner_content.
|
# `Phoenix.View.render_layout/4` is a phoenix function used for rendering partials that contain inner_content.
|
||||||
# These are usually layouts, but components that contain children are basically the same thing
|
# These are usually layouts, but components that contain children are basically the same thing
|
||||||
test "renders components using Phoenix.View.render_layout" do
|
test "renders components using Phoenix.View.render_layout" do
|
||||||
result =
|
result =
|
||||||
|
@ -20,7 +20,7 @@ defmodule Temple.ComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
~s{<div class="font-bold">Hello, world</div><%= Phoenix.View.render_layout Temple.Components.Component, :self, [] do %><aside class="foobar">I'm a component!</aside><% end %>}
|
~s|<div class="font-bold">Hello, world</div><%= Phoenix.View.render_layout Temple.Components.Component, :self, [{:__temple_slots__, %{}} \| []] do %><aside class="foobar">I'm a component!</aside><% end %>|
|
||||||
|
|
||||||
assert evaluate_template(result) ==
|
assert evaluate_template(result) ==
|
||||||
~s{<div class="font-bold">Hello, world</div><div><aside class="foobar">I'm a component!</aside></div>}
|
~s{<div class="font-bold">Hello, world</div><div><aside class="foobar">I'm a component!</aside></div>}
|
||||||
|
@ -39,7 +39,7 @@ defmodule Temple.ComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
~s{<div class="font-bold">Hello, world</div><%= Phoenix.View.render_layout Temple.Components.Component2, :self, [class: "bg-red"] do %>I'm a component!<% end %>}
|
~s|<div class="font-bold">Hello, world</div><%= Phoenix.View.render_layout Temple.Components.Component2, :self, [{:__temple_slots__, %{}} \| [class: "bg-red"]] do %>I'm a component!<% end %>|
|
||||||
|
|
||||||
assert evaluate_template(result) ==
|
assert evaluate_template(result) ==
|
||||||
~s{<div class="font-bold">Hello, world</div><div class="bg-red">I'm a component!</div>}
|
~s{<div class="font-bold">Hello, world</div><div class="bg-red">I'm a component!</div>}
|
||||||
|
@ -60,7 +60,7 @@ defmodule Temple.ComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
~s{<div class="font-bold">Hello, world</div><% class = "bg-red" %><%= Phoenix.View.render_layout Temple.Components.Component2, :self, [class: class] do %>I'm a component!<% end %>}
|
~s|<div class="font-bold">Hello, world</div><% class = "bg-red" %><%= Phoenix.View.render_layout Temple.Components.Component2, :self, [{:__temple_slots__, %{}} \| [class: class]] do %>I'm a component!<% end %>|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "function components can use other components" do
|
test "function components can use other components" do
|
||||||
|
@ -76,7 +76,7 @@ defmodule Temple.ComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
~s{<%= Phoenix.View.render_layout Temple.Components.Outer, :self, [] do %>outer!\n<% end %><%= Phoenix.View.render_layout Temple.Components.Inner, :self, [outer_id: "set by root inner"] do %>inner!\n<% end %>}
|
~s|<%= Phoenix.View.render_layout Temple.Components.Outer, :self, [{:__temple_slots__, %{}} \| []] do %>outer!\n<% end %><%= Phoenix.View.render_layout Temple.Components.Inner, :self, [{:__temple_slots__, %{}} \| [outer_id: "set by root inner"]] do %>inner!\n<% end %>|
|
||||||
|
|
||||||
assert evaluate_template(result) == ~s"""
|
assert evaluate_template(result) == ~s"""
|
||||||
<div id="inner" outer-id="from-outer">outer!</div>
|
<div id="inner" outer-id="from-outer">outer!</div>
|
||||||
|
@ -105,7 +105,7 @@ defmodule Temple.ComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
~s{<%= Phoenix.View.render_layout Temple.Components.WithFuncs, :self, [foo: :bar] do %>doo doo<% end %>}
|
~s|<%= Phoenix.View.render_layout Temple.Components.WithFuncs, :self, [{:__temple_slots__, %{}} \| [foo: :bar]] do %>doo doo<% end %>|
|
||||||
|
|
||||||
assert evaluate_template(result) == ~s{<div class="barbarbar">doo doo</div>}
|
assert evaluate_template(result) == ~s{<div class="barbarbar">doo doo</div>}
|
||||||
end
|
end
|
||||||
|
@ -117,8 +117,33 @@ defmodule Temple.ComponentTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
assert result ==
|
assert result ==
|
||||||
~s{<%= Phoenix.View.render Temple.Components.VoidComponent, :self, [foo: :bar] %>}
|
~s|<%= Phoenix.View.render Temple.Components.VoidComponent, :self, [{:__temple_slots__, %{}} \| [foo: :bar]] %>|
|
||||||
|
|
||||||
assert evaluate_template(result) == ~s{<div class="void!!">bar</div>}
|
assert evaluate_template(result) == ~s{<div class="void!!">bar</div>}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "components can have named slots" do
|
||||||
|
assigns = %{name: "bob"}
|
||||||
|
|
||||||
|
result =
|
||||||
|
temple do
|
||||||
|
c Temple.Components.WithSlot do
|
||||||
|
slot :header, %{value: val} do
|
||||||
|
div do
|
||||||
|
"the value is #{val}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
button class: "btn", phx_click: :toggle do
|
||||||
|
@name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert result ==
|
||||||
|
~s|<%= Phoenix.View.render_layout Temple.Components.WithSlot, :self, [{:__temple_slots__, %{header: fn %{value: val} -> %>\n<div>\n<%= "the value is \#{val}" %>\n</div><% end, }} \| []] do %>\n<button class="btn" phx-click="toggle">\n<%= @name %>\n\n</button>\n<% end %>|
|
||||||
|
|
||||||
|
assert evaluate_template(result, assigns) ==
|
||||||
|
~s{<div><div>the value is Header</div><div class="wrapped"><button class="btn" phx-click="toggle">bob</button></div></div>}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Temple.Parser.ComponentsTest do
|
defmodule Temple.Parser.ComponentsTest do
|
||||||
use ExUnit.Case, async: false
|
use ExUnit.Case, async: false
|
||||||
alias Temple.Parser.Components
|
alias Temple.Parser.Components
|
||||||
|
alias Temple.Parser.Slottable
|
||||||
use Temple.Support.Utils
|
use Temple.Support.Utils
|
||||||
|
|
||||||
describe "applicable?/1" do
|
describe "applicable?/1" do
|
||||||
|
@ -104,6 +105,32 @@ defmodule Temple.Parser.ComponentsTest do
|
||||||
children: []
|
children: []
|
||||||
} = ast
|
} = ast
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "gathers all slots" do
|
||||||
|
raw_ast =
|
||||||
|
quote do
|
||||||
|
c SomeModule, foo: :bar do
|
||||||
|
slot :foo, %{form: form} do
|
||||||
|
"in the slot"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ast = Components.run(raw_ast)
|
||||||
|
|
||||||
|
assert %Components{
|
||||||
|
module: SomeModule,
|
||||||
|
assigns: [foo: :bar],
|
||||||
|
slots: [
|
||||||
|
%Slottable{
|
||||||
|
name: :foo,
|
||||||
|
content: [%Temple.Parser.Text{}],
|
||||||
|
assigns: {:%{}, _, [form: _]}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
children: []
|
||||||
|
} = ast
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Temple.Generator.to_eex/1" do
|
describe "Temple.Generator.to_eex/1" do
|
||||||
|
@ -121,7 +148,53 @@ defmodule Temple.Parser.ComponentsTest do
|
||||||
|> Temple.Generator.to_eex()
|
|> Temple.Generator.to_eex()
|
||||||
|
|
||||||
assert result |> :erlang.iolist_to_binary() ==
|
assert result |> :erlang.iolist_to_binary() ==
|
||||||
~s|<%= Phoenix.View.render_layout SomeModule, :self, [foo: :bar] do %>\nI'm a component!\n<% end %>|
|
~s|<%= Phoenix.View.render_layout SomeModule, :self, [{:__temple_slots__, %{}} \| [foo: :bar]] do %>\nI'm a component!\n<% end %>|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "emits eex for void component with slots" do
|
||||||
|
raw_ast =
|
||||||
|
quote do
|
||||||
|
c SomeModule, foo: :bar do
|
||||||
|
slot :foo, %{form: form} do
|
||||||
|
div do
|
||||||
|
"in the slot"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result =
|
||||||
|
raw_ast
|
||||||
|
|> Components.run()
|
||||||
|
|> Temple.Generator.to_eex()
|
||||||
|
|
||||||
|
assert result |> :erlang.iolist_to_binary() ==
|
||||||
|
~s|<%= Phoenix.View.render SomeModule, :self, [{:__temple_slots__, %{foo: fn %{form: form} -> %><div>\nin the slot\n\n</div><% end, }} \| [foo: :bar]] %>|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "emits eex for nonvoid component with slots" do
|
||||||
|
raw_ast =
|
||||||
|
quote do
|
||||||
|
c SomeModule, foo: :bar do
|
||||||
|
slot :foo, %{form: form} do
|
||||||
|
div do
|
||||||
|
"in the slot"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
div do
|
||||||
|
"inner content"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result =
|
||||||
|
raw_ast
|
||||||
|
|> Components.run()
|
||||||
|
|> Temple.Generator.to_eex()
|
||||||
|
|
||||||
|
assert result |> :erlang.iolist_to_binary() ==
|
||||||
|
~s|<%= Phoenix.View.render_layout SomeModule, :self, [{:__temple_slots__, %{foo: fn %{form: form} -> %><div>\nin the slot\n\n</div><% end, }} \| [foo: :bar]] do %>\n<div>\ninner content</div>\n<% end %>|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "emits eex for void component" do
|
test "emits eex for void component" do
|
||||||
|
@ -136,7 +209,7 @@ defmodule Temple.Parser.ComponentsTest do
|
||||||
|> Temple.Generator.to_eex()
|
|> Temple.Generator.to_eex()
|
||||||
|
|
||||||
assert result |> :erlang.iolist_to_binary() ==
|
assert result |> :erlang.iolist_to_binary() ==
|
||||||
~s|<%= Phoenix.View.render SomeModule, :self, [foo: :bar] %>|
|
~s|<%= Phoenix.View.render SomeModule, :self, [{:__temple_slots__, %{}} \| [foo: :bar]] %>|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
48
test/parser/slot_test.exs
Normal file
48
test/parser/slot_test.exs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
defmodule Temple.Parser.SlotTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
alias Temple.Parser.Slot
|
||||||
|
|
||||||
|
describe "applicable?/1" do
|
||||||
|
test "runs when using the `c` ast with a block" do
|
||||||
|
ast =
|
||||||
|
quote do
|
||||||
|
slot :header, value: "yolo"
|
||||||
|
end
|
||||||
|
|
||||||
|
assert Slot.applicable?(ast)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "run/2" do
|
||||||
|
test "adds a node to the buffer" do
|
||||||
|
raw_ast =
|
||||||
|
quote do
|
||||||
|
slot :header, value: "yolo"
|
||||||
|
end
|
||||||
|
|
||||||
|
ast = Slot.run(raw_ast)
|
||||||
|
|
||||||
|
assert %Slot{
|
||||||
|
name: :header,
|
||||||
|
args: [value: "yolo"]
|
||||||
|
} == ast
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Temple.Generator.to_eex/1" do
|
||||||
|
test "emits eex for a slot" do
|
||||||
|
raw_ast =
|
||||||
|
quote do
|
||||||
|
slot :header, value: Form.form_for(changeset, action)
|
||||||
|
end
|
||||||
|
|
||||||
|
result =
|
||||||
|
raw_ast
|
||||||
|
|> Slot.run()
|
||||||
|
|> Temple.Generator.to_eex()
|
||||||
|
|
||||||
|
assert result |> :erlang.iolist_to_binary() ==
|
||||||
|
~s|<%= @__temple_slots__.header.(Enum.into([value: Form.form_for(changeset, action)], %{})) %>|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
24
test/parser/utils_test.exs
Normal file
24
test/parser/utils_test.exs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule Temple.Parser.UtilsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Temple.Parser.Utils
|
||||||
|
|
||||||
|
describe "runtime_attrs/1" do
|
||||||
|
test "compiles keyword lists and maps into html attributes" do
|
||||||
|
attrs_map = %{
|
||||||
|
class: "text-red",
|
||||||
|
id: "form1",
|
||||||
|
__temple_slots__: %{}
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs_kw = [
|
||||||
|
class: "text-red",
|
||||||
|
id: "form1",
|
||||||
|
__temple_slots__: %{}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert {:safe, ~s| class="text-red" id="form1"|} == Utils.runtime_attrs(attrs_map)
|
||||||
|
assert {:safe, ~s| class="text-red" id="form1"|} == Utils.runtime_attrs(attrs_kw)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
test/support/components/with_slot.ex
Normal file
13
test/support/components/with_slot.ex
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
defmodule Temple.Components.WithSlot do
|
||||||
|
use Temple.Component
|
||||||
|
|
||||||
|
render do
|
||||||
|
div do
|
||||||
|
slot :header, value: "Header"
|
||||||
|
|
||||||
|
div class: "wrapped" do
|
||||||
|
@inner_content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,10 +20,10 @@ defmodule Temple.Support.Utils do
|
||||||
Kernel.=~(a, b)
|
Kernel.=~(a, b)
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate_template(template) do
|
def evaluate_template(template, assigns \\ %{}) do
|
||||||
template
|
template
|
||||||
|> EEx.compile_string(engine: Phoenix.HTML.Engine)
|
|> EEx.compile_string(engine: Phoenix.HTML.Engine)
|
||||||
|> Code.eval_quoted([])
|
|> Code.eval_quoted(assigns: assigns)
|
||||||
|> elem(0)
|
|> elem(0)
|
||||||
|> Phoenix.HTML.safe_to_string()
|
|> Phoenix.HTML.safe_to_string()
|
||||||
end
|
end
|
||||||
|
|
Reference in a new issue