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:
parent
85eb81944e
commit
07c82e21d3
|
@ -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]
|
||||
]
|
||||
|
|
|
@ -6,7 +6,7 @@ on:
|
|||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
name: Test (${{matrix.elixir}}/${{matrix.otp}})
|
||||
|
||||
strategy:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}]
|
||||
|
|
|
@ -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
|
|
@ -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
15
mix.exs
|
@ -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
|
||||
|
|
1
mix.lock
1
mix.lock
|
@ -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"},
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'm a slot attribute</span>
|
||||
<p>
|
||||
motchy boi's in the footer!
|
||||
motchy boi'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'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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
Reference in New Issue