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:
parent
ba49ce2b4b
commit
265c413960
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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} =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
5
mix.exs
5
mix.exs
|
@ -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
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -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"},
|
||||
|
|
12
test/support/components/has_temple_function_assign.ex
Normal file
12
test/support/components/has_temple_function_assign.ex
Normal 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
|
|
@ -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
|
||||
|
|
Reference in a new issue