Allow defcomponent to work with runtime values for assigns

Also allows tags and defcomponents to accept maps in addition to keyword
lists
This commit is contained in:
Mitchell Hanberg 2019-07-04 00:16:29 -04:00
parent 6c8246fe89
commit 8daf85fdb3
6 changed files with 99 additions and 21 deletions

View file

@ -2,7 +2,7 @@ defmodule Mix.Tasks.Temple.Gen.Html do
@shortdoc "Generates controller, views, and context for an HTML resource in Temple"
@moduledoc """
Generates controller, views, and context for an HTML resource Temple.
Generates controller, views, and context for an HTML resource in Temple.
mix temple.gen.html Accounts User users name:string age:integer

View file

@ -115,7 +115,7 @@ defmodule Temple do
## Assigns
Components accept a keyword list of assigns and can be referenced in the body of the component by a module attribute of the same name.
Components accept a keyword list or a map of assigns and can be referenced in the body of the component by a module attribute of the same name.
This works exactly the same as EEx templates.
@ -149,28 +149,41 @@ defmodule Temple do
"""
defmacro defcomponent(name, [do: _] = block) do
quote do
defmacro unquote(name)(props \\ []) do
defmacro unquote(name)() do
outer = unquote(Macro.escape(block))
name = unquote(name)
{inner, props} = Keyword.pop(props, :do, nil)
quote do
unquote(name)(unquote(props), unquote(inner))
_ = unquote(outer)
end
end
defmacro unquote(name)(props_or_block)
defmacro unquote(name)([{:do, inner}]) do
name = unquote(name)
quote do
unquote(name)([], unquote(inner))
end
end
defmacro unquote(name)(props) do
name = unquote(name)
quote do
unquote(name)(unquote(props), nil)
end
end
defmacro unquote(name)(props, inner) do
import Kernel, except: [div: 2]
outer =
unquote(Macro.escape(block))
|> Macro.prewalk(&Temple.Utils.insert_props(&1, [{:children, inner} | props]))
|> Macro.prewalk(&Temple.Utils.insert_props(&1, props, inner))
name = unquote(name)
quote do
unquote(outer)
_ = unquote(outer)
end
end
end

View file

@ -6,7 +6,7 @@ defmodule Temple.Tags do
## Attributes
Tags accept a keyword list of attributes to be emitted into the element's opening tag. Multi-word attribute keys written in snake_case (`data_url`) will be transformed into kebab-case (`data-url`).
Tags accept a keyword list or a map of attributes to be emitted into the element's opening tag. Multi-word attribute keys written in snake_case (`data_url`) will be transformed into kebab-case (`data-url`).
## Children
@ -21,8 +21,11 @@ defmodule Temple.Tags do
# empty non-void element
div()
# non-void element with attributes
# non-void element with keyword list attributes
div class: "text-red", id: "my-el"
#
# non-void element with map attributes
div %{:class => "text-red", "id" => "my-el"}
# non-void element with children
div do

View file

@ -1,7 +1,7 @@
defmodule Temple.Utils do
@moduledoc false
def put_open_tag(buff, el, attrs) when is_list(attrs) do
def put_open_tag(buff, el, attrs) when is_list(attrs) or is_map(attrs) do
put_buffer(buff, "<#{el}#{compile_attrs(attrs)}>")
end
@ -23,17 +23,23 @@ defmodule Temple.Utils do
partial |> Phoenix.HTML.html_escape() |> Phoenix.HTML.safe_to_string()
end
def insert_props({:@, _, [{name, _, _}]}, props) when is_atom(name) do
props[name]
def insert_props({:@, _, [{:children, _, _}]}, _, inner) do
inner
end
def insert_props(ast, _inner), do: ast
def insert_props({:@, _, [{name, _, _}]}, props, _) when is_atom(name) do
quote do
Access.get(unquote_splicing([props, name]))
end
end
def insert_props(ast, _, _), do: ast
def compile_attrs([]), do: ""
def compile_attrs(attrs) do
for {name, value} <- attrs, into: "" do
name = name |> Atom.to_string() |> String.replace("_", "-")
name = name |> to_string() |> String.replace("_", "-")
" " <> name <> "=\"" <> to_string(value) <> "\""
end

View file

@ -1,4 +1,4 @@
defmodule Temple.HtmlTest do
defmodule Temple.TagsTest do
use ExUnit.Case, async: true
use Temple
@ -101,6 +101,17 @@ defmodule Temple.HtmlTest do
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
test "renders an attribute passed in as a map on a div" do
{:safe, result} =
htm do
div %{class: "hello"} do
div %{"class" => "hi"}
end
end
assert result == ~s{<div class="hello"><div class="hi"></div></div>}
end
test "renders an attribute on a div passed as a variable" do
attrs1 = [class: "hello"]
attrs2 = [class: "hi"]

View file

@ -104,8 +104,7 @@ defmodule TempleTest do
variable_as_prop(bob: bob)
end
assert result ==
~s|<div id="hi"></div>|
assert result == ~s|<div id="hi"></div>|
end
test "can pass a variable as a prop to a component with a block" do
@ -122,5 +121,51 @@ defmodule TempleTest do
assert result == ~s|<div id="hi"><div></div></div>|
end
test "can pass all of the props as a variable" do
import Component
props = [bob: "hi"]
{:safe, result} =
htm do
variable_as_prop(props)
end
assert result == ~s|<div id="hi"></div>|
end
test "can pass all of the props as a variable with a block" do
import Component
props = [bob: "hi"]
{:safe, result} =
htm do
variable_as_prop_with_block props do
div()
end
end
assert result == ~s|<div id="hi"><div></div></div>|
end
test "can pass a map as props with a block" do
import Component
props = %{bob: "hi"}
{:safe, result} =
htm do
variable_as_prop_with_block props do
div()
end
variable_as_prop_with_block %{bob: "hi"} do
div()
end
end
assert result == ~s|<div id="hi"><div></div></div><div id="hi"><div></div></div>|
end
end
end