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:
Mitchell Hanberg 2021-04-16 00:16:00 -04:00
parent f7197ede4a
commit 851f6415fe
26 changed files with 457 additions and 80 deletions

View file

@ -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

View file

@ -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/)

View file

@ -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

View file

@ -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())

View file

@ -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

View file

@ -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.

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -10,6 +10,5 @@ defmodule TempleDemo.Repo.Migrations.CreatePosts do
timestamps() timestamps()
end end
end end
end end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)},

View file

@ -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
View 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

View file

@ -0,0 +1,3 @@
defmodule Temple.Parser.Slottable do
defstruct content: nil, assigns: Macro.escape(%{}), name: nil
end

View file

@ -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) <> "\""

View file

@ -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

View file

@ -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
View 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

View 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

View 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

View file

@ -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