Allow Components to take props + other stuff
- Don't allow htm to take safe option - Always escape calls to `text`
This commit is contained in:
parent
8e1ae6cf11
commit
cada669071
3 changed files with 153 additions and 76 deletions
|
@ -24,13 +24,7 @@ defmodule Dsl.Html do
|
|||
area br col embed hr img input keygen param source track wbr
|
||||
]a
|
||||
|
||||
defmacro htm(opts) do
|
||||
quote do
|
||||
htm(unquote(Keyword.get(opts, :safe, false)), unquote(opts[:do]))
|
||||
end
|
||||
end
|
||||
|
||||
defmacro htm(safe?, block) do
|
||||
defmacro htm(do: block) do
|
||||
quote do
|
||||
import Kernel, except: [div: 2]
|
||||
|
||||
|
@ -42,11 +36,7 @@ defmodule Dsl.Html do
|
|||
|
||||
:ok = stop_buffer(var!(buff, Dsl.Html))
|
||||
|
||||
if unquote(safe?) do
|
||||
markup |> Enum.reverse() |> Enum.join("") |> HTML.html_escape()
|
||||
else
|
||||
markup |> Enum.reverse() |> Enum.join("")
|
||||
end
|
||||
markup |> Enum.reverse() |> Enum.join("")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -90,7 +80,7 @@ defmodule Dsl.Html do
|
|||
|
||||
defmacro text(text) do
|
||||
quote do
|
||||
put_buffer(var!(buff, Dsl.Html), to_string(unquote(text)))
|
||||
put_buffer(var!(buff, Dsl.Html), unquote(text) |> to_string |> HTML.html_escape |> HTML.safe_to_string())
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -98,41 +88,28 @@ defmodule Dsl.Html do
|
|||
|
||||
defmacro component(name, do: block) do
|
||||
quote do
|
||||
defmacro unquote(name)(attrs \\ [])
|
||||
defmacro unquote(name)(props \\ [])
|
||||
|
||||
defmacro unquote(name)(attrs) do
|
||||
defmacro unquote(name)(props) do
|
||||
outer = unquote(Macro.escape(block))
|
||||
name = unquote(name)
|
||||
|
||||
{inner, attrs} = Keyword.pop(attrs, :do, nil)
|
||||
|
||||
inner =
|
||||
case inner do
|
||||
{_, _, inner} ->
|
||||
inner
|
||||
|
||||
nil ->
|
||||
nil
|
||||
end
|
||||
{inner, props} = Keyword.pop(props, :do, nil)
|
||||
|
||||
quote do
|
||||
unquote(name)(unquote(attrs), unquote(inner))
|
||||
unquote(name)(unquote(props), unquote(inner))
|
||||
end
|
||||
end
|
||||
|
||||
defmacro unquote(name)(attrs, inner) do
|
||||
outer = unquote(Macro.escape(block))
|
||||
defmacro unquote(name)(props, inner) do
|
||||
import Kernel, except: [div: 2]
|
||||
|
||||
outer =
|
||||
unquote(Macro.escape(block))
|
||||
|> Macro.prewalk(&insert_props(&1, [{:children, inner} | props]))
|
||||
|
||||
name = unquote(name)
|
||||
|
||||
{tag, meta, [old_attrs]} = outer
|
||||
|
||||
attrs = [
|
||||
{:do, inner}
|
||||
| Keyword.merge(old_attrs, attrs, fn _, two, three -> two <> " " <> three end)
|
||||
]
|
||||
|
||||
outer = {tag, meta, [attrs]}
|
||||
|
||||
quote do
|
||||
unquote(outer)
|
||||
end
|
||||
|
@ -140,6 +117,12 @@ defmodule Dsl.Html do
|
|||
end
|
||||
end
|
||||
|
||||
def insert_props({:@, _, [{name, _, _}]}, props) when is_atom(name) do
|
||||
props[name]
|
||||
end
|
||||
|
||||
def insert_props(ast, _inner), do: ast
|
||||
|
||||
defp compile_attrs([]), do: ""
|
||||
|
||||
defp compile_attrs(attrs) do
|
||||
|
|
5
mix.exs
5
mix.exs
|
@ -5,12 +5,17 @@ defmodule Dsl.MixProject do
|
|||
[
|
||||
app: :dsl,
|
||||
version: "0.1.0",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
elixir: "~> 1.8",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
# Specifies which paths to compile per environment.
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
|
|
|
@ -2,6 +2,62 @@ defmodule Dsl.HtmlTest do
|
|||
use ExUnit.Case, async: true
|
||||
use Dsl
|
||||
|
||||
defmodule Component do
|
||||
component :flex do
|
||||
div(class: "flex")
|
||||
end
|
||||
|
||||
component :takes_children do
|
||||
div do
|
||||
div(id: "static-child-1")
|
||||
@children
|
||||
div(id: "static-child-2")
|
||||
end
|
||||
end
|
||||
|
||||
component :arbitrary_code do
|
||||
num = 1..10 |> Enum.reduce(0, fn x, sum -> x + sum end)
|
||||
|
||||
div do
|
||||
text(num)
|
||||
end
|
||||
end
|
||||
|
||||
component :uses_conditionals do
|
||||
if @condition do
|
||||
div()
|
||||
else
|
||||
span()
|
||||
end
|
||||
end
|
||||
|
||||
component :arbitrary_data do
|
||||
for item <- @lists do
|
||||
div do
|
||||
text(inspect(item))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
component :safe do
|
||||
div()
|
||||
end
|
||||
|
||||
component :safe_with_prop do
|
||||
div id: "safe-with-prop" do
|
||||
text(@prop)
|
||||
|
||||
div do
|
||||
span do
|
||||
for x <- @lists do
|
||||
div(do: text(x))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "non-void elements" do
|
||||
test "renders two divs" do
|
||||
result =
|
||||
|
@ -97,14 +153,13 @@ defmodule Dsl.HtmlTest do
|
|||
end
|
||||
|
||||
describe "escaping" do
|
||||
test "marks as safe" do
|
||||
{safe?, result} =
|
||||
htm safe: true do
|
||||
div()
|
||||
test "text is excaped" do
|
||||
result =
|
||||
htm do
|
||||
text "<div>Text</div>"
|
||||
end
|
||||
|
||||
assert safe? == :safe
|
||||
assert IO.iodata_to_binary(result) == ~s{<div></div>}
|
||||
assert result == ~s{<div>Text</div>}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -129,15 +184,9 @@ defmodule Dsl.HtmlTest do
|
|||
end
|
||||
end
|
||||
|
||||
defmodule CustomTag do
|
||||
component :flex do
|
||||
div(class: "flex")
|
||||
end
|
||||
end
|
||||
|
||||
describe "custom tags" do
|
||||
test "defines a basic tag that acts as partial" do
|
||||
import CustomTag
|
||||
describe "custom component" do
|
||||
test "defines a basic component" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
|
@ -147,44 +196,84 @@ defmodule Dsl.HtmlTest do
|
|||
assert result == ~s{<div class="flex"></div>}
|
||||
end
|
||||
|
||||
test "defines a tag that takes children" do
|
||||
import CustomTag
|
||||
test "defines a component that takes 1 child" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
flex do
|
||||
div()
|
||||
div()
|
||||
takes_children do
|
||||
div(id: "dynamic-child")
|
||||
end
|
||||
end
|
||||
|
||||
assert result == ~s{<div class="flex"><div></div><div></div></div>}
|
||||
assert result ==
|
||||
~s{<div><div id="static-child-1"></div><div id="dynamic-child"></div><div id="static-child-2"></div></div>}
|
||||
end
|
||||
|
||||
test "defines a tag that has attributes" do
|
||||
import CustomTag
|
||||
test "defines a component that takes multiple children" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
flex(class: "justify-between", id: "king")
|
||||
end
|
||||
|
||||
assert result =~ ~s{class="flex justify-between"}
|
||||
assert result =~ ~s{id="king"}
|
||||
end
|
||||
|
||||
test "defines a tag that has attributes AND children" do
|
||||
import CustomTag
|
||||
|
||||
result =
|
||||
htm do
|
||||
flex class: "justify-between" do
|
||||
div()
|
||||
div()
|
||||
takes_children do
|
||||
div(id: "dynamic-child-1")
|
||||
div(id: "dynamic-child-2")
|
||||
end
|
||||
end
|
||||
|
||||
assert result == ~s{<div class="flex justify-between"><div></div><div></div></div>}
|
||||
assert result ==
|
||||
~s{<div><div id="static-child-1"></div><div id="dynamic-child-1"></div><div id="dynamic-child-2"></div><div id="static-child-2"></div></div>}
|
||||
end
|
||||
|
||||
test "can access a prop" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
takes_children name: "mitch" do
|
||||
text(@name)
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div><div id="static-child-1"></div>mitch<div id="static-child-2"></div></div>}
|
||||
end
|
||||
|
||||
test "can have arbitrary code inside the definition" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
arbitrary_code()
|
||||
end
|
||||
|
||||
assert result == ~s{<div>55</div>}
|
||||
end
|
||||
|
||||
test "can use conditionals to render different markup" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
uses_conditionals(condition: true)
|
||||
uses_conditionals(condition: false)
|
||||
end
|
||||
|
||||
assert result == ~s{<div></div><span></span>}
|
||||
end
|
||||
|
||||
test "can pass arbitrary data as props" do
|
||||
import Component
|
||||
|
||||
result =
|
||||
htm do
|
||||
arbitrary_data(
|
||||
lists: [:atom, %{key: "value"}, {:status, :tuple}, "string", 1, [1, 2, 3]]
|
||||
)
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s|<div>:atom</div><div>%{key: "value"}</div><div>{:status, :tuple}</div><div>"string"</div><div>1</div><div>[1, 2, 3]</div>|
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Reference in a new issue