Dynamic Attributes (#190)

* Move directories for ast tests to match convention

* feat!: Rename `:let` to `:let!`

We use the "bang" style as the reserved keyword to differentiate it from
other possible attributes.

* feat: use Phoenix.HTML as the default engine

I am choosing to leverage this library in order to quickly get dynamic
attributes (see #183) up and running.

This also ensures that folks who wish to use Temple outside of a Phoenix
project with get some nice HTML functions as well as properly escaped
HTML out of the box.

This can be made optional if Temple becomes decoupled from the render
and it including HTML specific packages becomes a strange.

* feat: Allow user to make their own Component module

The component module is essentially to defer compiling functions that the
user might not need. The component, render_slot, and inner_block functions
are only mean to be used when there isn't another implementation.

In the case of a LiveView application, LiveView is providing the
component runtime implementation. This was causing some compile time
warnings for temple, because it was using the LiveView engine at compile
time (for Temple, not the user's application) and LiveView hadn't been
compiled or loaded.

So, now we defer this to the user to make their own module and import it
where necessary.

* feat: Pass dynamic attributes with the :rest! attribute

The :rest! attribute can be used to pass in a dynamic list of attributes
to be mixed into the static ones at runtime.

Since this cannot be properly escaped by any engine, we have to mark it
as safe and then allow the function to escape it for us. I decided to
leverage the `attributes_escape/1` function from `phoenix_html`. There
isn't really any point in making my own version of this or vendoring it.

Now you can also pass a variable as the attributes as well if you only
want to pass through attributes from a calling component.

The :rest! attribute also works with components, allowing you to pass
a dynamic list of args into them.

Fixes #183

* Move test components to their own file.

* docs(components): Update documentation on Temple.Components

* docs(guides): Mention attributes_escape/1 function in the guides

* chore(test): Move helper to it's own module

* feat: rest! support for slots

* docs(guides): Dynamic attributes

* ci: downgrade runs-on to support OTP 23
This commit is contained in:
Mitchell Hanberg 2023-01-21 06:44:29 -05:00 committed by GitHub
parent 85eb81944e
commit 07c82e21d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 449 additions and 221 deletions

View File

@ -121,6 +121,6 @@ locals_without_parens = Enum.map(temple ++ html ++ svg, &{&1, :*})
[
import_deps: [:typed_struct],
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: locals_without_parens,
locals_without_parens: locals_without_parens ++ [assert_html: 2],
export: [locals_without_parens: locals_without_parens]
]

View File

@ -6,7 +6,7 @@ on:
jobs:
tests:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Test (${{matrix.elixir}}/${{matrix.otp}})
strategy:

View File

@ -6,7 +6,7 @@
- Rendering slots is now done by passing the assign with the slot name to the `slot` keyword instead of name as an atom. If this slot has multiple definitions, you can loop through them and render each one individually, or render them all at once. Please see the migration guide for more information.
- The `:default` slot has been renamed to `:inner_block`. This is to be easily compatible with HEEx/Surface. Please see the migration guide for more information.
- Capturing the data being passed into a slot is now defined using the `:let` attribute. Please see the migration guide for more information.
- Capturing the data being passed into a slot is now defined using the `:let!` attribute. Please see the migration guide for more information.
### Enhancements

View File

@ -26,7 +26,7 @@ end
To use a component, you will use the special `c` keyword. This is called a "keyword" because it is not a function or macro, but only exists inside of the `Temple.temple/1` block.
The first argument will be the function reference to your component function, followed by any assigns.
The first argument will be the function reference to your component function, followed by any assigns. You can pass dynamic assigns using the `:rest!` keyword the same way you would with a normal tag.
```elixir
defmodule MyApp.ConfirmDialog do
@ -156,7 +156,7 @@ end
## Passing data to and through Slots
Sometimes it is necessary to pass data _into_ a slot (hereby known as *slot attributes*) from the call site and _from_ a component definition (hereby known as *slot arguments*) back to the call site.
Sometimes it is necessary to pass data _into_ a slot (hereby known as *slot attributes*) from the call site and _from_ a component definition (hereby known as *slot arguments*) back to the call site. Dynamic slot attributes can be passed using the `:rest!` attribute in the same way you can with tag attributes.
Let's look at what a `table` component could look like. Here we observe we access an attribute in the slot in the header with `col.label`.
@ -214,11 +214,11 @@ def MyApp.TableExample do
c &table/1, rows: @users do
# 👇 defining the parameter for the slot argument
slot :col, let: user, label: "Name" do # 👈 passing a slot attribute
slot :col, let!: user, label: "Name" do # 👈 passing a slot attribute
user.name
end
slot :col, let: user, label: "Address" do
slot :col, let!: user, label: "Address" do
user.address
end
end

View File

@ -64,7 +64,7 @@ end
The syntax for capturing data being passed from the call site of a slot to the definition of a slot (or put another way, from the definition of a component to the call site of the component) has changed.
You now capture it as the value of the `:let` attribute on the slot definition.
You now capture it as the value of the `:let!` attribute on the slot definition.
### Before
@ -86,7 +86,7 @@ end
def my_component(assign) do
temple do
c &my_component/1 do
slot :a_slot, let: %{some: value} do
slot :a_slot, let!: %{some: value} do
"I'm using some #{value}"
end
end

View File

@ -121,54 +121,34 @@ end
## Attributes
Attributes are declared as a keyword list.
Temple leverages `Phoenix.HTML.attributes_escape/1` internally, so you can refer to it's documentation for all of the details.
- Keys with underscores are converted to the kebab syntax.
- Values can be Elixir expressions.
- Values that evaluate to `true` will be emitted as a boolean attribute. `disabled` and `checked` are examples of boolean attributes.
- Values that evaluate `false` will not be emitted into the document at all.
- The class attribute has a special "object syntax" that allows you to specify classes as a keyword list, only emitting classes that evaluate to true into the final class.
### Dynamic Attributes
Let's look at an example.
To render dynamic attributes into a tag, you can pass them with the reserved attribute `:rest!`.
```elixir
assigns = %{highlight?: false, user_name: "Mitch"}
assigns = %
data: [data_foo: "hi"]
}
temple do
div id: "hero" do
h2 class: "font-bold", do: "Profile"
section data_controller: "hero" do
p class: ["border": @highlight?] do
"Name: #{@user_name}"
end
video autoplay: true, src: "https://example.com/rick-rolled.mp4"
end
div id: "foo", rest!: @data do
"Hello, world!"
end
end
```
...will emit markup that looks like...
will render to
```html
<div id="hero">
<h2 class="font-bold">Profile</h2>
<section data-controller="hero">
<p class="">
Name: Mitch
</p>
</section>
<video autoplay src="https://example.com/rick-rolled.mp4"></video>
<div id="foo" data-foo="hi">
Hello, world!
</div>
```
## Elixir Expressions
### They Just Work
Any Elixir expression can be used anywhere inside of a Temple template. Here are a few examples.
```elixir

View File

@ -1,6 +1,4 @@
defmodule Temple do
@engine Application.compile_env(:temple, :engine, EEx.SmartEngine)
@moduledoc """
Temple syntax is available inside the `temple`, and is compiled into efficient Elixir code at compile time using the configured `EEx.Engine`.
@ -93,82 +91,21 @@ defmodule Temple do
<link href="/css/site.css">
```
"""
@doc false
def engine(), do: @engine
defmacro temple(block) do
opts = [engine: engine()]
quote do
require Temple.Renderer
Temple.Renderer.compile(unquote(opts), unquote(block))
Temple.Renderer.compile(unquote(block))
|> then(fn
{:safe, template} ->
template
template ->
template
end)
end
end
@doc false
def component(func, assigns, _) do
apply(func, [assigns])
end
defmacro inner_block(_name, do: do_block) do
__inner_block__(do_block)
end
@doc false
def __inner_block__([{:->, meta, _} | _] = do_block) do
inner_fun = {:fn, meta, do_block}
quote do
fn arg ->
_ = var!(assigns)
unquote(inner_fun).(arg)
end
end
end
def __inner_block__(do_block) do
quote do
fn arg ->
_ = var!(assigns)
unquote(do_block)
end
end
end
defmacro render_slot(slot, arg) do
quote do
unquote(__MODULE__).__render_slot__(unquote(slot), unquote(arg))
end
end
@doc false
def __render_slot__([], _), do: nil
def __render_slot__([entry], argument) do
call_inner_block!(entry, argument)
end
def __render_slot__(entries, argument) when is_list(entries) do
assigns = %{}
_ = assigns
temple do
for entry <- entries do
call_inner_block!(entry, argument)
end
end
end
def __render_slot__(entry, argument) when is_map(entry) do
entry.inner_block.(argument)
end
defp call_inner_block!(entry, argument) do
if !entry.inner_block do
message = "attempted to render slot #{entry.__slot__} but the slot has no inner content"
raise RuntimeError, message
end
entry.inner_block.(argument)
end
defdelegate engine, to: Temple.Renderer
end

View File

@ -40,7 +40,7 @@ defmodule Temple.Ast.Components do
if is_nil(slot) do
{node, {component_function, named_slots}}
else
{parameter, attributes} = Keyword.pop(arguments || [], :let)
{parameter, attributes} = Keyword.pop(arguments || [], :let!)
new_slot = {name, %{parameter: parameter, slot: slot, attributes: attributes}}
{nil, {component_function, named_slots ++ [new_slot]}}
end

View File

@ -34,7 +34,7 @@ defmodule Temple.Ast.Utils do
[
{:expr,
quote do
unquote(List.first(attrs))
Phoenix.HTML.attributes_escape(unquote(List.first(attrs)))
end}
]
end
@ -48,14 +48,25 @@ defmodule Temple.Ast.Utils do
[]
end
def build_attr("rest!", values) when is_list(values) do
Enum.flat_map(values, fn {name, value} ->
build_attr(snake_to_kebab(name), value)
end)
end
def build_attr("rest!", {_, _, _} = value) do
expr =
quote do
Phoenix.HTML.attributes_escape(unquote(value))
end
[{:expr, expr}]
end
def build_attr(name, {_, _, _} = value) do
expr =
quote do
case unquote(value) do
true -> " " <> unquote(name)
false -> ""
_ -> ~s' #{unquote(name)}="#{unquote(value)}"'
end
Phoenix.HTML.attributes_escape([{unquote(name), unquote(value)}])
end
[{:expr, expr}]

124
lib/temple/component.ex Normal file
View File

@ -0,0 +1,124 @@
defmodule Temple.Component do
@moduledoc """
Use this module to create your own component implementation.
This is only required if you are not using a component implementation from another framework,
like Phoenix LiveView.
At it's core, a component implmentation includes the following functions
- `component/2`
- `inner_block/2`
- `render_slot/2`
These functions are used by the template compiler, so you won't be calling them directly.
## Usage
Invoke the `__using__/1` macro to create your own module, and then import that module where you
need to define define or use components (usually everywhere).
We'll use an example that is similar to what Temple uses in its own test suite..
```elixir
defmodule MyAppWeb.Component do
use Temple.Component
defmacro __using__(_) do
quote do
import Temple
import unquote(__MODULE__)
end
end
end
```
Then you can `use` your module when you want to define or use a component.
```elixir
defmodule MyAppWeb.Components do
use MyAppWeb.Component
def basic_component(_assigns) do
temple do
div do
"I am a basic component"
end
end
end
end
```
"""
defmacro __using__(_) do
quote do
import Temple
@doc false
def component(func, assigns, _) do
{:safe, apply(func, [assigns])}
end
defmacro inner_block(_name, do: do_block) do
__inner_block__(do_block)
end
@doc false
def __inner_block__([{:->, meta, _} | _] = do_block) do
inner_fun = {:fn, meta, do_block}
quote do
fn arg ->
_ = var!(assigns)
unquote(inner_fun).(arg)
end
end
end
def __inner_block__(do_block) do
quote do
fn arg ->
_ = var!(assigns)
unquote(do_block)
end
end
end
defmacro render_slot(slot, arg) do
quote do
unquote(__MODULE__).__render_slot__(unquote(slot), unquote(arg))
end
end
@doc false
def __render_slot__([], _), do: nil
def __render_slot__([entry], argument) do
call_inner_block!(entry, argument)
end
def __render_slot__(entries, argument) when is_list(entries) do
assigns = %{}
_ = assigns
temple do
for entry <- entries do
call_inner_block!(entry, argument)
end
end
end
def __render_slot__(entry, argument) when is_map(entry) do
entry.inner_block.(argument)
end
defp call_inner_block!(entry, argument) do
if !entry.inner_block do
message = "attempted to render slot #{entry.__slot__} but the slot has no inner content"
raise RuntimeError, message
end
entry.inner_block.(argument)
end
end
end
end

View File

@ -17,12 +17,14 @@ defmodule Temple.Renderer do
alias Temple.Ast.Utils
@default_engine EEx.SmartEngine
@engine Application.compile_env(:temple, :engine, Phoenix.HTML.Engine)
@doc false
def engine(), do: @engine
defmacro compile(opts \\ [], do: block) do
defmacro compile(do: block) do
block
|> Temple.Parser.parse()
|> Temple.Renderer.render(opts)
|> Temple.Renderer.render(engine: @engine)
# |> Temple.Ast.Utils.inspect_ast()
end
@ -30,7 +32,7 @@ defmodule Temple.Renderer do
def render(asts, opts \\ [])
def render(asts, opts) when is_list(asts) and is_list(opts) do
engine = Keyword.get(opts, :engine, @default_engine)
engine = Keyword.get(opts, :engine, Phoenix.HTML.Engine)
state = %{
engine: engine,
@ -97,13 +99,22 @@ defmodule Temple.Renderer do
end
end
{:%{}, [],
[
__slot__: slot.name,
inner_block: inner_block
] ++ slot.attributes}
{rest, attributes} = Keyword.pop(slot.attributes, :rest!, [])
slot =
{:%{}, [],
[
__slot__: slot.name,
inner_block: inner_block
] ++ attributes}
quote do
Map.merge(unquote(slot), Map.new(unquote(rest)))
end
end)
{rest, arguments} = Keyword.pop(arguments, :rest!, [])
component_arguments =
{:%{}, [],
arguments
@ -111,6 +122,11 @@ defmodule Temple.Renderer do
|> Map.merge(slot_quotes)
|> Enum.to_list()}
component_arguments =
quote do
Map.merge(unquote(component_arguments), Map.new(unquote(rest)))
end
expr =
quote do
component(
@ -126,7 +142,9 @@ defmodule Temple.Renderer do
def render(buffer, state, %Slot{} = ast) do
render_slot_func =
quote do
render_slot(unquote(ast.name), unquote(ast.args))
{rest, args} = Map.pop(Map.new(unquote(ast.args)), :rest!, [])
args = Map.merge(args, Map.new(rest))
render_slot(unquote(ast.name), args)
end
state.engine.handle_expr(buffer, "=", render_slot_func)

15
mix.exs
View File

@ -18,7 +18,17 @@ defmodule Temple.MixProject do
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(:test) do
# hack to get the right compiler options used on the non-script files in
# test/support
Code.put_compiler_option(
:parser_options,
Keyword.put(Code.get_compiler_option(:parser_options), :token_metadata, true)
)
["lib", "test/support"]
end
defp elixirc_paths(_), do: ["lib"]
# Run "mix help compile.app" to learn about applications.
@ -61,8 +71,9 @@ defmodule Temple.MixProject do
defp deps do
[
{:typed_struct, "~> 0.3"},
{:floki, ">= 0.0.0"},
{:phoenix_html, "~> 3.2"},
{:typed_struct, "~> 0.3"},
{:ex_doc, "~> 0.29.0", only: :dev, runtime: false}
]
end

View File

@ -7,5 +7,6 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
"typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
}

10
test/support/component.ex Normal file
View File

@ -0,0 +1,10 @@
defmodule Temple.Support.Component do
use Temple.Component
defmacro __using__(_) do
quote do
import Temple
import unquote(__MODULE__)
end
end
end

View File

@ -0,0 +1,54 @@
defmodule Temple.Support.Components do
use Temple.Support.Component
def basic_component(_assigns) do
temple do
div do
"I am a basic component"
end
end
end
def default_slot(assigns) do
temple do
div do
"I am above the slot"
slot @inner_block
end
end
end
def named_slot(assigns) do
temple do
div do
"#{@name} is above the slot"
slot @inner_block
end
footer do
for f <- @footer do
span do: f[:label]
slot f, %{name: @name}
end
end
end
end
def rest_component(assigns) do
temple do
div do
"I am a basic #{@id} with #{@class}"
end
end
end
def rest_slot(assigns) do
temple do
div do
for foo <- @foo do
slot foo, slot_id: foo.id, rest!: [slot_class: foo.class]
end
end
end
end
end

13
test/support/helpers.ex Normal file
View File

@ -0,0 +1,13 @@
defmodule Temple.Support.Helpers do
defmacro assert_html(expected, actual) do
quote do
assert unquote(expected) == Phoenix.HTML.safe_to_string(unquote(actual)), """
--- Expected ---
#{unquote(expected)}----------------
--- Actual ---
#{Phoenix.HTML.safe_to_string(unquote(actual))}--------------
"""
end
end
end

View File

@ -111,7 +111,7 @@ defmodule Temple.Ast.ComponentsTest do
raw_ast =
quote do
c unquote(func), foo: :bar do
slot :foo, let: %{form: form} do
slot :foo, let!: %{form: form} do
"in the slot"
end
end
@ -136,7 +136,7 @@ defmodule Temple.Ast.ComponentsTest do
raw_ast =
quote do
c unquote(func), foo: :bar do
slot :foo, let: %{form: form}, label: the_label do
slot :foo, let!: %{form: form}, label: the_label do
"in the slot"
end
end
@ -170,7 +170,7 @@ defmodule Temple.Ast.ComponentsTest do
c unquote(list), socials: @user.socials do
"hello"
slot :foo, let: %{text: text, url: url} do
slot :foo, let!: %{text: text, url: url} do
a class: "text-blue-500 hover:underline", href: url do
text
end

View File

@ -25,11 +25,7 @@ defmodule Temple.Ast.UtilsTest do
assert Macro.to_string(
quote do
case @class do
true -> " " <> "class"
false -> ""
_ -> ~s' #{"class"}="#{@class}"'
end
Phoenix.HTML.attributes_escape([{"class", unquote(class_ast)}])
end
) == Macro.to_string(actual)
end
@ -60,5 +56,27 @@ defmodule Temple.Ast.UtilsTest do
# the ast metadata is different, let's just compare stringified versions
assert Macro.to_string(result_expr) == Macro.to_string(expr)
end
test "the rest! attribute will mix in the values at runtime" do
rest_ast =
quote do
rest
end
attrs = [class: "text-red", rest!: rest_ast]
actual = Utils.compile_attrs(attrs)
assert [
{:text, ~s' class="text-red"'},
{:expr, rest_actual}
] = actual
assert Macro.to_string(
quote do
Phoenix.HTML.attributes_escape(unquote(rest_ast))
end
) == Macro.to_string(rest_actual)
end
end
end

View File

@ -1,11 +1,14 @@
defmodule Temple.RendererTest do
use ExUnit.Case, async: true
import Temple
use Temple.Support.Component
import Temple.Support.Components
require Temple.Renderer
alias Temple.Renderer
import Temple.Support.Helpers
describe "compile/1" do
test "produces renders a text node" do
result =
@ -13,7 +16,7 @@ defmodule Temple.RendererTest do
"hello world"
end
assert "hello world\n" == result
assert_html "hello world\n", result
end
test "produces renders a div" do
@ -36,7 +39,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "produces renders a void elements" do
@ -61,7 +64,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "a match does not emit" do
@ -83,7 +86,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "handles simple expression inside attributes" do
@ -104,29 +107,29 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
# test "handles simple expression are the entire attributes" do
# assigns = %{statement: "hello world", attributes: [class: "green"]}
test "handles simple expression are the entire attributes" do
assigns = %{statement: "hello world", attributes: [class: "green"]}
# result =
# Renderer.compile do
# div @attributes do
# @statement
# end
# end
result =
Renderer.compile do
div @attributes do
@statement
end
end
# # html
# expected = """
# <div class="green">
# hello world
# </div>
# html
expected = """
<div class="green">
hello world
</div>
# """
"""
# assert expected == result
# end
assert_html expected, result
end
test "handles simple expression with @ assign" do
assigns = %{statement: "hello world"}
@ -146,7 +149,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "handles multi line expression" do
@ -172,7 +175,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "if expression" do
@ -199,7 +202,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
end
@ -232,7 +235,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
end
@ -264,7 +267,7 @@ defmodule Temple.RendererTest do
end
end
assert expected == result
assert_html expected, result
end
test "handles anonymous functions" do
@ -293,7 +296,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
def super_map(enumerable, func, _extra_args) do
@ -330,15 +333,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
end
def basic_component(_assigns) do
temple do
div do
"I am a basic component"
end
end
assert_html expected, result
end
test "basic component" do
@ -361,16 +356,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
end
def default_slot(assigns) do
temple do
div do
"I am above the slot"
slot @inner_block
end
end
assert_html expected, result
end
test "component with default slot" do
@ -399,23 +385,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
end
def named_slot(assigns) do
temple do
div do
"#{@name} is above the slot"
slot @inner_block
end
footer do
for f <- @footer do
span do: f[:label]
slot f, %{name: @name}
end
end
end
assert_html expected, result
end
test "component with a named slot" do
@ -427,7 +397,7 @@ defmodule Temple.RendererTest do
c &named_slot/1, name: "motchy boi" do
span do: "i'm a slot"
slot :footer, let: %{name: name}, label: @label, expr: 1 + 1 do
slot :footer, let!: %{name: name}, label: @label, expr: 1 + 1 do
p do
"#{name}'s in the footer!"
end
@ -446,9 +416,9 @@ defmodule Temple.RendererTest do
</div>
<footer>
<span>i'm a slot attribute</span>
<span>i&#39;m a slot attribute</span>
<p>
motchy boi's in the footer!
motchy boi&#39;s in the footer!
</p>
</footer>
@ -458,7 +428,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
end
@ -479,7 +449,7 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "boolean attributes only emit correctly with truthy values" do
@ -493,7 +463,7 @@ defmodule Temple.RendererTest do
<input type="text" disabled placeholder="Enter some text...">
"""
assert expected == result
assert_html expected, result
end
test "boolean attributes don't emit with falsy values" do
@ -507,7 +477,7 @@ defmodule Temple.RendererTest do
<input type="text" placeholder="Enter some text...">
"""
assert expected == result
assert_html expected, result
end
test "runtime boolean attributes emit the right values" do
@ -524,7 +494,7 @@ defmodule Temple.RendererTest do
<input type="text" checked placeholder="Enter some text...">
"""
assert expected == result
assert_html expected, result
end
test "multiple slots" do
@ -536,13 +506,13 @@ defmodule Temple.RendererTest do
c &named_slot/1, name: "motchy boi" do
span do: "i'm a slot"
slot :footer, let: %{name: name} do
slot :footer, let!: %{name: name} do
p do
"#{name}'s in the footer!"
end
end
slot :footer, let: %{name: name} do
slot :footer, let!: %{name: name} do
p do
"#{name} is the second footer!"
end
@ -563,7 +533,7 @@ defmodule Temple.RendererTest do
<footer>
<span></span>
<p>
motchy boi's in the footer!
motchy boi&#39;s in the footer!
</p>
<span></span>
<p>
@ -577,7 +547,87 @@ defmodule Temple.RendererTest do
"""
assert expected == result
assert_html expected, result
end
test "rest! attribute can mix in dynamic attrs with the static attrs" do
assigns = %{
rest: [
class: "font-bold",
disabled: true
]
}
result =
Renderer.compile do
div id: "foo", rest!: @rest do
"hi"
end
end
# heex
expected = """
<div id="foo" class="font-bold" disabled>
hi
</div>
"""
assert_html expected, result
end
test "rest! attribute can mix in dynamic assigns to components" do
assigns = %{
rest: [
class: "font-bold"
]
}
result =
Renderer.compile do
c &rest_component/1, id: "foo", rest!: @rest
end
# heex
expected = """
<div>
I am a basic foo with font-bold
</div>
"""
assert_html expected, result
end
test "rest! attribute can mix in dynamic attributes to slots" do
assigns = %{
rest: [
class: "font-bold"
]
}
result =
Renderer.compile do
c &rest_slot/1 do
slot :foo,
id: "passed-into-slot",
rest!: @rest,
let!: %{slot_class: class, slot_id: id} do
"id is #{id} and class is #{class}"
end
end
end
# heex
expected = """
<div>
id is passed-into-slot and class is font-bold
</div>
"""
assert_html expected, result
end
end
end

View File

@ -4,21 +4,22 @@ defmodule TempleTest do
describe "temple/1" do
test "works" do
assigns = %{name: "mitch"}
assigns = %{name: "mitch", extra: [foo: "bar"]}
result =
temple do
div class: "hello" do
div class: "hi" do
div class: "hello", rest!: [id: "hi", name: @name] do
div class: "hi", rest!: @extra do
@name
end
end
end
|> :erlang.iolist_to_binary()
# heex
expected = """
<div class="hello">
<div class="hi">
<div class="hello" id="hi" name="mitch">
<div class="hi" foo="bar">
mitch
</div>