Allow element attrs to be evaluated at runtime

Before this change, only keyword list literals could be passed to
elements. If they had non-literals as values, then those would compile
to EEx expressions.

This allows a non-literal to be passed as attrs and have the entire thing
compile to an EEx expression, which will pass the non-literal to a
"runtime_attrs" function, which evaluates a keyword list into a safe
string.

That last part might need to be reworked if the user is not using
the Phoenix.HTML.Engine EEx Engine.
This commit is contained in:
Mitchell Hanberg 2020-08-09 10:07:27 -04:00
parent ba49ce2b4b
commit 265c413960
9 changed files with 134 additions and 16 deletions

View file

@ -2,6 +2,30 @@
## Master
- Can pass a keyword list to be evaluated at runtime as attrs/assigns to an element.
```elixir
# compile time
div class: "foo", id: bar do
# something
end
# <div class="foo" id="<%= bar %>">
# <!-- something -->
# </div>
# runtime
div some_var do
# something
end
# <div<%= PrivateTempleModule.runtime_attrs(some_var) %>>
# <!-- something -->
# </div>
```
### Breaking
Components are now defined using modules. You can convert your existing components by configuring your component prefix and wrapping your current component files in the `Temple.Component` behaviour implementation.

View file

@ -100,21 +100,36 @@ defmodule Temple.Parser do
compile_attrs(attrs)
end
def compile_attrs(attrs) do
for {name, value} <- attrs, into: "" do
name = snake_to_kebab(name)
def compile_attrs(attrs) when is_list(attrs) do
if Keyword.keyword?(attrs) do
for {name, value} <- attrs, into: "" do
name = snake_to_kebab(name)
case value do
{_, _, _} = macro ->
" " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
case value do
{_, _, _} = macro ->
" " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
value ->
" " <> name <> "=\"" <> to_string(value) <> "\""
value ->
" " <> name <> "=\"" <> to_string(value) <> "\""
end
end
else
"<%= Temple.Parser.Private.runtime_attrs(" <>
(attrs |> List.first() |> Macro.to_string()) <> ") %>"
end
end
def split_args(not_what_i_want) when is_nil(not_what_i_want) or is_atom(not_what_i_want), do: {[], []}
def runtime_attrs(attrs) do
{:safe,
for {name, value} <- attrs, into: "" do
name = snake_to_kebab(name)
" " <> name <> "=\"" <> to_string(value) <> "\""
end}
end
def split_args(not_what_i_want) when is_nil(not_what_i_want) or is_atom(not_what_i_want),
do: {[], []}
def split_args(args) do
{do_and_else, args} =

View file

@ -22,13 +22,13 @@ defmodule Temple.Parser.Components do
{assigns, block}
[[do: block]] ->
{nil, block}
{[], block}
[assigns] ->
{assigns, nil}
_ ->
{nil, nil}
{[], nil}
end
component_module = Module.concat([@component_prefix, Macro.camelize(to_string(name))])
@ -45,7 +45,7 @@ defmodule Temple.Parser.Components do
assigns
{:@, _, [{name, _, _}]} = node ->
if !is_nil(assigns) && name in Keyword.keys(assigns) do
if name in Keyword.keys(assigns) do
Keyword.get(assigns, name, nil)
else
node

View file

@ -21,7 +21,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do
{do_and_else, args} =
case args do
[args] ->
[args] when is_list(args) ->
{do_value, args} = Keyword.pop(args, :do)
do_and_else = Keyword.put_new(do_and_else, :do, do_value)

View file

@ -22,7 +22,7 @@ defmodule Temple.Parser.TempleNamespaceNonvoid do
{do_and_else, args} =
case args do
[args] ->
[args] when is_list(args) ->
{do_value, args} = Keyword.pop(args, :do)
do_and_else = Keyword.put_new(do_and_else, :do, do_value)

View file

@ -46,8 +46,9 @@ defmodule Temple.MixProject do
defp deps do
[
{:ex_doc, "~> 0.22.0", only: [:dev], runtime: false},
{:phoenix, ">= 0.0.0", optional: true}
{:ex_doc, "~> 0.22.0", only: :dev, runtime: false},
{:phoenix, ">= 0.0.0", optional: true},
{:phoenix_html, ">= 0.0.0", only: :test}
]
end
end

View file

@ -6,6 +6,7 @@
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},

View file

@ -0,0 +1,12 @@
defmodule Temple.Components.HasTempleFunctionAssign do
@behaviour Temple.Component
@impl Temple.Component
def render do
quote do
div Keyword.put(@temple, :class, "flex #{@temple[:class]}") do
@children
end
end
end
end

View file

@ -367,4 +367,69 @@ defmodule TempleTest do
assert result ==
~s{<%= for(x <- 1..5, y <- 6..10) do %><div><%= x %></div><div><%= y %></div><% end %>}
end
test "can pass an expression as assigns" do
result =
temple do
fieldset if true == false, do: [disabled: true], else: [] do
input type: "text"
end
end
assert result ==
~s{<fieldset<%= Temple.Parser.Private.runtime_attrs(if(true == false) do [disabled: true]else []end) %>><input type="text"></fieldset>}
end
test "can pass a variable as assigns" do
result =
temple do
fieldset foo_bar do
input type: "text"
end
end
assert result ==
~s{<fieldset<%= Temple.Parser.Private.runtime_attrs(foo_bar) %>><input type="text"></fieldset>}
end
test "can pass a function as assigns" do
result =
temple do
fieldset Foo.foo_bar() do
input type: "text"
end
end
assert result ==
~s{<fieldset<%= Temple.Parser.Private.runtime_attrs(Foo.foo_bar()) %>><input type="text"></fieldset>}
end
test "can pass a function as assigns that has @temple" do
result =
temple do
has_temple_function_assign class: "justify-end", style: "color: pink" do
input type: "text"
end
end
expected =
~S"""
<div<%= Temple.Parser.Private.runtime_attrs(Keyword.put([class: "justify-end", style: "color: pink"], :class, "flex #{[class: "justify-end", style: "color: pink"][:class]}")) %>>
<input type="text">
</div>
"""
|> String.trim()
assert result == expected
assert evaluate_template(result) == evaluate_template(expected)
end
defp evaluate_template(template) do
template
|> EEx.compile_string(engine: Phoenix.HTML.Engine)
|> Code.eval_quoted()
|> elem(0)
|> Phoenix.HTML.safe_to_string()
end
end