Align component model with HEEx/Surface (#182)

* Align component model with HEEx/Surface

This change aligns the component model with HEEx/Surface. This shoudl
allow one to interop components created in any syntax with any other
syntax.

The advantage of this is folks can utilize component packages created
using a different syntax.

This includes several enhancements and breaking changes, please see the changelog and the migration guide for further details.

Closes #130
This commit is contained in:
Mitchell Hanberg 2022-10-12 09:17:23 -04:00 committed by GitHub
parent dca47b9802
commit db231e7b6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 497 additions and 235 deletions

View file

@ -2,6 +2,19 @@
## Main
### Enhancements
- Temple components are now compatible with HEEx/Surface components! Some small tweaks to the component implementation has made this possible. Please see the guides for more information.
- Multiple instances of the same slot name can now be declared and then rendered inside the component (similar to HEEx and Surface).
- You can now pass arbitrary data to slots, and it does not need to be a map or a keyword list. I don't think this is a breaking change, but please submit an issue if you notice it is.
- Slot attributes. You can now pass data into a slot from the definition site and use it at the call site (inside the component).
### Breaking Changes
- 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.
### 0.10.0
### Enhancements

View file

@ -67,6 +67,8 @@ Temple components are simple to write and easy to use.
Unlike normal partials, Temple components have the concept of "slots", which are similar [Vue](https://v3.vuejs.org/guide/component-slots.html#named-slots). You can also refer to HEEx and Surface for examples of templates that have the "slot" concept.
Temple components are compatible with HEEx and Surface components and can be shared.
Please see the [guides](https://hexdocs.pm/temple/components.html) for more details.
```elixir
@ -77,15 +79,15 @@ defmodule MyAppWeb.Component do
temple do
section do
div do
slot :header
slot @header
end
div do
slot :default
slot @inner_block
end
div do
slot :footer
slot @footer
end
end
end

View file

@ -54,7 +54,7 @@ Slots are defined and rendered using the `slot` keyword. This is similar to the
### Default Slot
The default slot can be rendered from within your component by passing the `slot` the atom `:default`. Let's redefine our button component using slots.
The default slot can be rendered from within your component by passing the `slot` the `@inner_block` assign. Let's redefine our button component using slots.
```elixir
defmodule MyApp.Components do
@ -63,7 +63,7 @@ defmodule MyApp.Components do
def button(assigns) do
temple do
button type: "button", class: "bg-blue-800 text-white rounded #{@class}" do
slot :default
slot @inner_block
end
end
end
@ -109,18 +109,18 @@ defmodule MyApp.Components do
div class: "card" do
header class: "card-header", style: "background-color: @f5f5f5" do
p class: "card-header-title" do
slot :header
slot @header
end
end
div class: "card-content" do
div class: "content" do
slot :default
slot @inner_block
end
end
footer class: "card-footer", style: "background-color: #f5f5f5" do
slot :footer
slot @footer
end
end
end
@ -145,8 +145,8 @@ def MyApp.CardExample do
"This example demonstrates how to create components with multiple, named slots"
slot :footer do
a href="#", class: "card-footer-item", do: "Footer Item 1"
a href="#", class: "card-footer-item", do: "Footer Item 2"
a href: "#", class: "card-footer-item", do: "Footer Item 1"
a href: "#", class: "card-footer-item", do: "Footer Item 2"
end
end
end
@ -154,11 +154,15 @@ def MyApp.CardExample do
end
```
## Passing Data Through Slots
## Passing data to and through Slots
Sometimes it is necessary to pass data from a component definition 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.
Let's look at what a `table` component could look like.
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`.
This example is taken from the HEEx documentation to demonstrate how you can build the same thing with Temple.
Note: Slot attributes can only be accessed on an individual slot, so if you define a single slot definition, you still need to loop through it to access it, as they are stored as a list.
#### Definition
@ -166,30 +170,23 @@ Let's look at what a `table` component could look like.
defmodule MyApp.Components do
import Temple
def cols(items) do
items
|> List.first()
|> Map.keys()
|> Enum.sort()
end
def table(assigns) do
temple do
table do
thead do
tr do
for col <- cols(@entries) do
tr do: String.upcase(to_string(col))
for col <- @col do
th do: col.label # 👈 accessing a slot attribute
end
end
end
tbody do
for row <- @entries do
for row <- @rows do
tr do
for col <- cols(@entries) do
for col <- @col do
td do
slot :cell, %{value: row[cell]}
slot col, row
end
end
end
@ -203,7 +200,7 @@ end
#### Usage
When we render the slot, we can pattern match on the data passed through the slot. If this seems familiar, it's because this is the same syntax you use when writing your tests using `ExUnit.Case.test/3`.
When we render the slot, we can pattern match on the data passed through the slot via the `:let` attribute.
```elixir
def MyApp.TableExample do
@ -213,24 +210,16 @@ def MyApp.TableExample do
def render(assigns) do
temple do
section do
h2 do: "Inventory Levels"
h2 do: "Users"
c &table/1, entries: @item_inventories do
slot :cell, %{value: value} do
case value do
0 ->
span class: "font-bold" do
"Out of stock!"
end
c &table/1, rows: @users do
# 👇 defining the parameter for the slot argument
slot :col, let: user, label: "Name" do # 👈 passing a slot attribute
user.name
end
level when is_number(level) ->
span do
"#{level} in stock"
end
_ ->
span do: value
end
slot :col, let: user, label: "Address" do
user.address
end
end
end

View file

@ -0,0 +1,95 @@
# Migrating from 0.10 to 0.11
Most of the changes in this release are related to tweaking Temple's component model to align with HEEx & Surface.
## Rendering Slots
Slots are now available as assigns in the component and are rendered as such.
### Before
```elixir
def my_component(assign) do
temple do
span do
slot :a_slot
end
end
end
```
### After
```elixir
def my_component(assign) do
temple do
span do
slot @a_slot
end
end
end
```
## :default slot has been renamed to :inner_block
The main body of a component has been renamed from `:default` to `:inner_block`.
Note: The "after" example also includes the necessary change specified above.
### Before
```elixir
def my_component(assign) do
temple do
span do
slot :default
end
end
end
```
### After
```elixir
def my_component(assign) do
temple do
span do
slot @inner_block
end
end
end
```
## Passing data into slots
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.
### Before
```elixir
def my_component(assign) do
temple do
c &my_component/1 do
slot :a_slot, %{some: value} do
"I'm using some #{value}"
end
end
end
end
```
### After
```elixir
def my_component(assign) do
temple do
c &my_component/1 do
slot :a_slot, let: %{some: value} do
"I'm using some #{value}"
end
end
end
end
```

View file

@ -93,6 +93,8 @@ defmodule Temple do
<link href="/css/site.css">
```
"""
@doc false
def engine(), do: @engine
defmacro temple(block) do
opts = [engine: engine()]
@ -104,10 +106,69 @@ defmodule Temple do
end
@doc false
def component(func, assigns) do
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 engine(), do: @engine
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

View file

@ -2,17 +2,17 @@ defmodule Temple.Ast do
@moduledoc false
@type t ::
Temple.Parser.Empty.t()
| Temple.Parser.Text.t()
| Temple.Parser.Components.t()
| Temple.Parser.Slot.t()
| Temple.Parser.NonvoidElementsAliases.t()
| Temple.Parser.VoidElementsAliases.t()
| Temple.Parser.AnonymousFunctions.t()
| Temple.Parser.RightArrow.t()
| Temple.Parser.DoExpressions.t()
| Temple.Parser.Match.t()
| Temple.Parser.Default.t()
Temple.Ast.Empty.t()
| Temple.Ast.Text.t()
| Temple.Ast.Components.t()
| Temple.Ast.Slot.t()
| Temple.Ast.NonvoidElementsAliases.t()
| Temple.Ast.VoidElementsAliases.t()
| Temple.Ast.AnonymousFunctions.t()
| Temple.Ast.RightArrow.t()
| Temple.Ast.DoExpressions.t()
| Temple.Ast.Match.t()
| Temple.Ast.Default.t()
def new(module, opts \\ []) do
struct(module, opts)

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.AnonymousFunctions do
defmodule Temple.Ast.AnonymousFunctions do
@moduledoc false
@behaviour Temple.Parser
@ -11,7 +11,7 @@ defmodule Temple.Parser.AnonymousFunctions do
@impl true
def applicable?({_, _, args}) do
import Temple.Parser.Utils, only: [split_args: 1]
import Temple.Ast.Utils, only: [split_args: 1]
args
|> split_args()
@ -23,9 +23,9 @@ defmodule Temple.Parser.AnonymousFunctions do
@impl true
def run({_name, _, args} = expression) do
{_do_and_else, args} = Temple.Parser.Utils.split_args(args)
{_do_and_else, args} = Temple.Ast.Utils.split_args(args)
{_args, func_arg, _args2} = Temple.Parser.Utils.split_on_fn(args, {[], nil, []})
{_args, func_arg, _args2} = Temple.Ast.Utils.split_on_fn(args, {[], nil, []})
{_func, _, [{_arrow, _, [[{_arg, _, _}], block]}]} = func_arg

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Components do
defmodule Temple.Ast.Components do
@moduledoc false
@behaviour Temple.Parser
@ -6,8 +6,7 @@ defmodule Temple.Parser.Components do
typedstruct do
field :function, function()
field :assigns, map()
field :children, [map()]
field :arguments, map()
field :slots, [function()]
end
@ -22,27 +21,28 @@ defmodule Temple.Parser.Components do
def run({:c, _meta, [component_function | args]}) do
{do_and_else, args} =
args
|> Temple.Parser.Utils.split_args()
|> Temple.Ast.Utils.split_args()
{do_and_else, assigns} = Temple.Parser.Utils.consolidate_blocks(do_and_else, args)
{do_and_else, arguments} = Temple.Ast.Utils.consolidate_blocks(do_and_else, args)
{default_slot, {_, named_slots}} =
if children = do_and_else[:do] do
Macro.prewalk(
children,
{component_function, %{}},
{component_function, []},
fn
{:c, _, [name | _]} = node, {_, named_slots} ->
{node, {name, named_slots}}
{:slot, _, [name | args]} = node, {^component_function, named_slots} ->
{assigns, slot} = split_assigns_and_children(args, Macro.escape(%{}))
{arguments, slot} = split_assigns_and_children(args, nil)
if is_nil(slot) do
{node, {component_function, named_slots}}
else
{nil,
{component_function, Map.put(named_slots, name, %{assigns: assigns, slot: slot})}}
{parameter, attributes} = Keyword.pop(arguments || [], :let)
new_slot = {name, %{parameter: parameter, slot: slot, attributes: attributes}}
{nil, {component_function, named_slots ++ [new_slot]}}
end
node, acc ->
@ -57,37 +57,45 @@ defmodule Temple.Parser.Components do
if default_slot == nil do
[]
else
Temple.Parser.parse(default_slot)
[
Temple.Ast.new(
Temple.Ast.Slottable,
name: :inner_block,
content: Temple.Parser.parse(default_slot)
)
]
end
slots =
for {name, %{slot: slot, assigns: assigns}} <- named_slots do
for {name, %{slot: slot, parameter: parameter, attributes: attributes}} <- named_slots do
Temple.Ast.new(
Temple.Parser.Slottable,
Temple.Ast.Slottable,
name: name,
content: Temple.Parser.parse(slot),
assigns: assigns
parameter: parameter,
attributes: attributes
)
end
slots = children ++ slots
Temple.Ast.new(__MODULE__,
function: component_function,
assigns: assigns,
slots: slots,
children: children
arguments: arguments,
slots: slots
)
end
defp split_assigns_and_children(args, empty) do
case args do
[assigns, [do: block]] ->
{assigns, block}
[arguments, [do: block]] ->
{arguments, block}
[[do: block]] ->
{empty, block}
[assigns] ->
{assigns, nil}
[arguments] ->
{arguments, nil}
_ ->
{empty, nil}

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Default do
defmodule Temple.Ast.Default do
@moduledoc false
@behaviour Temple.Parser

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.DoExpressions do
defmodule Temple.Ast.DoExpressions do
@moduledoc false
@behaviour Temple.Parser
@ -18,7 +18,7 @@ defmodule Temple.Parser.DoExpressions do
@impl true
def run({name, meta, args}) do
{do_and_else, args} = Temple.Parser.Utils.split_args(args)
{do_and_else, args} = Temple.Ast.Utils.split_args(args)
do_body = Temple.Parser.parse(do_and_else[:do])

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.ElementList do
defmodule Temple.Ast.ElementList do
@moduledoc false
@behaviour Temple.Parser

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Empty do
defmodule Temple.Ast.Empty do
@moduledoc false
use TypedStruct

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Match do
defmodule Temple.Ast.Match do
@moduledoc false
@behaviour Temple.Parser

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.NonvoidElementsAliases do
defmodule Temple.Ast.NonvoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
@ -24,9 +24,9 @@ defmodule Temple.Parser.NonvoidElementsAliases do
def run({name, meta, args}) do
name = Parser.nonvoid_elements_lookup()[name]
{do_and_else, args} = Temple.Parser.Utils.split_args(args)
{do_and_else, args} = Temple.Ast.Utils.split_args(args)
{do_and_else, args} = Temple.Parser.Utils.consolidate_blocks(do_and_else, args)
{do_and_else, args} = Temple.Ast.Utils.consolidate_blocks(do_and_else, args)
children = Temple.Parser.parse(do_and_else[:do])
@ -35,7 +35,7 @@ defmodule Temple.Parser.NonvoidElementsAliases do
attrs: args,
meta: %{whitespace: whitespace(meta)},
children:
Temple.Ast.new(Temple.Parser.ElementList,
Temple.Ast.new(Temple.Ast.ElementList,
children: children,
whitespace: whitespace(meta)
)

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.RightArrow do
defmodule Temple.Ast.RightArrow do
@moduledoc false
@behaviour Temple.Parser

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Slot do
defmodule Temple.Ast.Slot do
@moduledoc false
@behaviour Temple.Parser
use TypedStruct

View file

@ -0,0 +1,12 @@
defmodule Temple.Ast.Slottable do
@moduledoc false
use TypedStruct
typedstruct do
field :content, [Temple.Ast.t()]
field :parameter, Macro.t()
field :name, atom()
field :attributes, Macro.t(), default: []
end
end

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.TempleNamespaceNonvoid do
defmodule Temple.Ast.TempleNamespaceNonvoid do
@moduledoc false
@behaviour Temple.Parser
@ -14,6 +14,6 @@ defmodule Temple.Parser.TempleNamespaceNonvoid do
@impl true
def run({name, meta, args}) do
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
Temple.Parser.NonvoidElementsAliases.run({name, meta, args})
Temple.Ast.NonvoidElementsAliases.run({name, meta, args})
end
end

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.TempleNamespaceVoid do
defmodule Temple.Ast.TempleNamespaceVoid do
@moduledoc false
@behaviour Temple.Parser
@ -13,6 +13,6 @@ defmodule Temple.Parser.TempleNamespaceVoid do
def run({name, meta, args}) do
{:., _, [{:__aliases__, _, [:Temple]}, name]} = name
Temple.Parser.VoidElementsAliases.run({name, meta, args})
Temple.Ast.VoidElementsAliases.run({name, meta, args})
end
end

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Text do
defmodule Temple.Ast.Text do
@moduledoc false
@behaviour Temple.Parser

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.Utils do
defmodule Temple.Ast.Utils do
@moduledoc false
def snake_to_kebab(stringable),
@ -25,7 +25,7 @@ defmodule Temple.Parser.Utils do
[{:text, " " <> name <> "=\"" <> to_string(value) <> "\""} | acc]
else
true ->
nodes = Temple.Parser.Utils.build_attr(name, value)
nodes = Temple.Ast.Utils.build_attr(name, value)
Enum.reverse(nodes) ++ acc
end
end

View file

@ -1,4 +1,4 @@
defmodule Temple.Parser.VoidElementsAliases do
defmodule Temple.Ast.VoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
@ -19,7 +19,7 @@ defmodule Temple.Parser.VoidElementsAliases do
@impl true
def run({name, _, args}) do
args =
case Temple.Parser.Utils.split_args(args) do
case Temple.Ast.Utils.split_args(args) do
{_, [args]} when is_list(args) ->
args

View file

@ -1,19 +1,19 @@
defmodule Temple.Parser do
@moduledoc false
alias Temple.Parser.AnonymousFunctions
alias Temple.Parser.Components
alias Temple.Parser.Default
alias Temple.Parser.DoExpressions
alias Temple.Parser.Empty
alias Temple.Parser.Match
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.RightArrow
alias Temple.Parser.Slot
alias Temple.Parser.TempleNamespaceNonvoid
alias Temple.Parser.TempleNamespaceVoid
alias Temple.Parser.Text
alias Temple.Parser.VoidElementsAliases
alias Temple.Ast.AnonymousFunctions
alias Temple.Ast.Components
alias Temple.Ast.Default
alias Temple.Ast.DoExpressions
alias Temple.Ast.Empty
alias Temple.Ast.Match
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.RightArrow
alias Temple.Ast.Slot
alias Temple.Ast.TempleNamespaceNonvoid
alias Temple.Ast.TempleNamespaceVoid
alias Temple.Ast.Text
alias Temple.Ast.VoidElementsAliases
@aliases Application.compile_env(:temple, :aliases, [])

View file

@ -1,11 +0,0 @@
defmodule Temple.Parser.Slottable do
@moduledoc false
use TypedStruct
typedstruct do
field :content, any()
field :assigns, map(), default: Macro.escape(%{})
field :name, atom()
end
end

View file

@ -1,20 +1,21 @@
defmodule Temple.Renderer do
@moduledoc false
alias Temple.Parser.ElementList
alias Temple.Parser.Text
alias Temple.Parser.Components
alias Temple.Parser.Slot
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.VoidElementsAliases
alias Temple.Parser.AnonymousFunctions
alias Temple.Parser.RightArrow
alias Temple.Parser.DoExpressions
alias Temple.Parser.Match
alias Temple.Parser.Default
alias Temple.Parser.Empty
alias Temple.Ast.ElementList
alias Temple.Ast.Text
alias Temple.Ast.Components
alias Temple.Ast.Slot
alias Temple.Ast.Slottable
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.VoidElementsAliases
alias Temple.Ast.AnonymousFunctions
alias Temple.Ast.RightArrow
alias Temple.Ast.DoExpressions
alias Temple.Ast.Match
alias Temple.Ast.Default
alias Temple.Ast.Empty
alias Temple.Parser.Utils
alias Temple.Ast.Utils
@default_engine EEx.SmartEngine
@ -23,7 +24,7 @@ defmodule Temple.Renderer do
|> Temple.Parser.parse()
|> Temple.Renderer.render(opts)
# |> Temple.Parser.Utils.inspect_ast()
# |> Temple.Ast.Utils.inspect_ast()
end
def render(asts, opts \\ [])
@ -37,7 +38,7 @@ defmodule Temple.Renderer do
terminal_node: false
}
buffer = engine.init(%{})
buffer = engine.init([])
buffer =
for ast <- asts, reduce: buffer do
@ -73,25 +74,11 @@ defmodule Temple.Renderer do
def render(buffer, state, %Components{
function: function,
assigns: assigns,
children: children,
arguments: arguments,
slots: slots
}) do
child_quoted =
if Enum.any?(children) do
children_buffer = state.engine.handle_begin(buffer)
children_buffer =
for child <- children(children), reduce: children_buffer do
children_buffer ->
render(children_buffer, state, child)
end
state.engine.handle_end(children_buffer)
end
slot_quotes =
for slot <- slots do
Enum.group_by(slots, & &1.name, fn %Slottable{} = slot ->
slot_buffer = state.engine.handle_begin(buffer)
slot_buffer =
@ -102,29 +89,34 @@ defmodule Temple.Renderer do
ast = state.engine.handle_end(slot_buffer)
[quoted] =
inner_block =
quote do
{unquote(slot.name), unquote(slot.assigns)} ->
unquote(ast)
inner_block unquote(slot.name) do
unquote(slot.parameter || quote(do: _)) ->
unquote(ast)
end
end
quoted
end
{:%{}, [],
[
__slot__: slot.name,
inner_block: inner_block
] ++ slot.attributes}
end)
{:fn, meta, clauses} =
quote do
fn
{:default, _} -> unquote(child_quoted)
end
end
slot_func = {:fn, meta, slot_quotes ++ clauses}
component_arguments =
{:%{}, [],
arguments
|> Map.new()
|> Map.merge(slot_quotes)
|> Enum.to_list()}
expr =
quote do
component(
unquote(function),
Map.put(Map.new(unquote(assigns)), :__slots__, unquote(slot_func))
unquote(component_arguments),
{__MODULE__, __ENV__.function, __ENV__.file, __ENV__.line}
)
end
@ -134,7 +126,7 @@ defmodule Temple.Renderer do
def render(buffer, state, %Slot{} = ast) do
render_slot_func =
quote do
var!(assigns).__slots__.({unquote(ast.name), Map.new(unquote(ast.args))})
render_slot(unquote(ast.name), unquote(ast.args))
end
state.engine.handle_expr(buffer, "=", render_slot_func)
@ -242,7 +234,7 @@ defmodule Temple.Renderer do
{name, meta, args} = ast.elixir_ast
{args, {func, fmeta, [{arrow, arrowmeta, [first, _block]}]}, args2} =
Temple.Parser.Utils.split_on_fn(args, {[], nil, []})
Temple.Ast.Utils.split_on_fn(args, {[], nil, []})
full_ast =
{name, meta, args ++ [{func, fmeta, [{arrow, arrowmeta, [first, inner_quoted]}]}] ++ args2}

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.AnonymousFunctionsTest do
defmodule Temple.Ast.AnonymousFunctionsTest do
use ExUnit.Case, async: true
alias Temple.Parser.AnonymousFunctions
alias Temple.Ast.AnonymousFunctions
describe "applicable?/1" do
test "returns true when the node contains an anonymous function as an argument to a function" do
@ -57,7 +57,7 @@ defmodule Temple.Parser.AnonymousFunctionsTest do
assert %AnonymousFunctions{
elixir_ast: _,
children: [
%Temple.Parser.Default{
%Temple.Ast.Default{
elixir_ast: ^expected_child
}
]

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.ComponentsTest do
use ExUnit.Case, async: false
alias Temple.Parser.Components
alias Temple.Parser.Slottable
defmodule Temple.Ast.ComponentsTest do
use ExUnit.Case, async: true
alias Temple.Ast.Components
alias Temple.Ast.Slottable
describe "applicable?/1" do
test "runs when using the `c` ast with a block" do
@ -57,8 +57,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [],
children: _
arguments: []
} = ast
end
@ -72,8 +71,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
children: _
arguments: [foo: :bar]
} = ast
end
@ -91,8 +89,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
children: _
arguments: [foo: :bar]
} = ast
end
@ -106,8 +103,7 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
children: []
arguments: [foo: :bar]
} = ast
end
@ -115,7 +111,7 @@ defmodule Temple.Parser.ComponentsTest do
raw_ast =
quote do
c unquote(func), foo: :bar do
slot :foo, %{form: form} do
slot :foo, let: %{form: form} do
"in the slot"
end
end
@ -125,15 +121,40 @@ defmodule Temple.Parser.ComponentsTest do
assert %Components{
function: ^func,
assigns: [foo: :bar],
arguments: [foo: :bar],
slots: [
%Slottable{
name: :foo,
content: [%Temple.Parser.Text{}],
assigns: {:%{}, _, [form: _]}
content: [%Temple.Ast.Text{}],
parameter: {:%{}, _, [form: _]}
}
],
children: []
]
} = ast
end
test "slot attributes", %{func: func} do
raw_ast =
quote do
c unquote(func), foo: :bar do
slot :foo, let: %{form: form}, label: the_label do
"in the slot"
end
end
end
ast = Components.run(raw_ast)
assert %Components{
function: ^func,
arguments: [foo: :bar],
slots: [
%Slottable{
name: :foo,
content: [%Temple.Ast.Text{}],
parameter: {:%{}, _, [form: _]},
attributes: [label: {:the_label, [], Temple.Ast.ComponentsTest}]
}
]
} = ast
end
@ -149,7 +170,7 @@ defmodule Temple.Parser.ComponentsTest do
c unquote(list), socials: @user.socials do
"hello"
slot :default, %{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
@ -161,16 +182,32 @@ defmodule Temple.Parser.ComponentsTest do
ast = Components.run(raw_ast)
assert Kernel.==(ast.slots, [])
assert [
%Slottable{
name: :inner_block,
parameter: nil
}
] = ast.slots
assert %Components{
children: [
%Components{
children: [
slots: [
%Slottable{
content: [
%Components{
slots: [
%Slottable{
name: :default
content: [
%Components{
slots: [
%Slottable{
name: :inner_block
},
%Slottable{
name: :foo
}
]
}
]
}
]
}

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.DefaultTest do
defmodule Temple.Ast.DefaultTest do
use ExUnit.Case, async: true
alias Temple.Parser.Default
alias Temple.Ast.Default
describe "applicable?/1" do
test "returns true when the node is an elixir expression" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.DoExpressionsTest do
defmodule Temple.Ast.DoExpressionsTest do
use ExUnit.Case, async: true
alias Temple.Parser.DoExpressions
alias Temple.Ast.DoExpressions
describe "applicable?/1" do
test "returns true when the node contains a do expression" do
@ -30,7 +30,7 @@ defmodule Temple.Parser.DoExpressionsTest do
assert %DoExpressions{
elixir_ast: _,
children: [
[%Temple.Parser.Text{text: "bob"}],
[%Temple.Ast.Text{text: "bob"}],
nil
]
} = ast

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.EmptyTest do
defmodule Temple.Ast.EmptyTest do
use ExUnit.Case, async: true
alias Temple.Parser.Empty
alias Temple.Ast.Empty
describe "applicable?/1" do
test "returns true when the node is non-content" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.MatchTest do
defmodule Temple.Ast.MatchTest do
use ExUnit.Case, async: true
alias Temple.Parser.Match
alias Temple.Ast.Match
describe "applicable?/1" do
test "returns true when the node is an elixir match expression" do

View file

@ -1,8 +1,9 @@
defmodule Temple.Parser.NonvoidElementsAliasesTest do
defmodule Temple.Ast.NonvoidElementsAliasesTest do
use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.ElementList
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.ElementList
alias Temple.Ast.Text
describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do
@ -74,7 +75,7 @@ defmodule Temple.Parser.NonvoidElementsAliasesTest do
name: "option",
children: %ElementList{
children: [
%Temple.Parser.Text{text: "foo"}
%Text{text: "foo"}
]
}
}

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.RightArrowTest do
defmodule Temple.Ast.RightArrowTest do
use ExUnit.Case, async: true
alias Temple.Parser.RightArrow
alias Temple.Ast.RightArrow
describe "applicable?/1" do
test "returns true when the node contains a right arrow" do
@ -52,7 +52,7 @@ defmodule Temple.Parser.RightArrowTest do
assert %RightArrow{
elixir_ast: {:->, [newlines: 1], [[:bing]]},
children: [
%Temple.Parser.Default{
%Temple.Ast.Default{
elixir_ast: ^bong
}
]

View file

@ -1,6 +1,6 @@
defmodule Temple.Parser.SlotTest do
use ExUnit.Case, async: false
alias Temple.Parser.Slot
defmodule Temple.Ast.SlotTest do
use ExUnit.Case, async: true
alias Temple.Ast.Slot
describe "applicable?/1" do
test "runs when using the `c` ast with a block" do

View file

@ -1,8 +1,10 @@
defmodule Temple.Parser.TempleNamespaceNonvoidTest do
defmodule Temple.Ast.TempleNamespaceNonvoidTest do
use ExUnit.Case, async: true
alias Temple.Parser.NonvoidElementsAliases
alias Temple.Parser.TempleNamespaceNonvoid
alias Temple.Ast.ElementList
alias Temple.Ast.NonvoidElementsAliases
alias Temple.Ast.TempleNamespaceNonvoid
alias Temple.Ast.Text
describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do
@ -50,8 +52,8 @@ defmodule Temple.Parser.TempleNamespaceNonvoidTest do
assert %NonvoidElementsAliases{
name: "div",
attrs: [class: "foo", id: {:var, [], _}],
children: %Temple.Parser.ElementList{
children: [%Temple.Parser.Text{text: "foo"}],
children: %ElementList{
children: [%Text{text: "foo"}],
whitespace: :loose
}
} = ast

View file

@ -1,8 +1,8 @@
defmodule Temple.Parser.TempleNamespaceVoidTest do
defmodule Temple.Ast.TempleNamespaceVoidTest do
use ExUnit.Case, async: true
alias Temple.Parser.TempleNamespaceVoid
alias Temple.Parser.VoidElementsAliases
alias Temple.Ast.TempleNamespaceVoid
alias Temple.Ast.VoidElementsAliases
describe "applicable?/1" do
test "returns true when the node is a Temple aliased nonvoid element" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.TextTest do
defmodule Temple.Ast.TextTest do
use ExUnit.Case, async: true
alias Temple.Parser.Text
alias Temple.Ast.Text
describe "applicable?/1" do
test "returns true when the node is a string literal" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.UtilsTest do
defmodule Temple.Ast.UtilsTest do
use ExUnit.Case, async: true
alias Temple.Parser.Utils
alias Temple.Ast.Utils
describe "compile_attrs/1" do
test "returns a list of text nodes for static attributes" do

View file

@ -1,7 +1,7 @@
defmodule Temple.Parser.VoidElementsAliasesTest do
defmodule Temple.Ast.VoidElementsAliasesTest do
use ExUnit.Case, async: true
alias Temple.Parser.VoidElementsAliases
alias Temple.Ast.VoidElementsAliases
describe "applicable?/1" do
test "returns true when the node is a nonvoid element or alias" do

View file

@ -368,12 +368,14 @@ defmodule Temple.RendererTest do
temple do
div do
"I am above the slot"
slot :default
slot @inner_block
end
end
end
test "component with default slot" do
assigns = %{}
result =
Renderer.compile do
div do
@ -404,23 +406,28 @@ defmodule Temple.RendererTest do
temple do
div do
"#{@name} is above the slot"
slot :default
slot @inner_block
end
footer do
slot :footer, %{name: @name}
for f <- @footer do
span do: f[:label]
slot f, %{name: @name}
end
end
end
end
test "component with a named slot" do
assigns = %{label: "i'm a slot attribute"}
result =
Renderer.compile do
div do
c &named_slot/1, name: "motchy boi" do
span do: "i'm a slot"
slot :footer, %{name: name} do
slot :footer, let: %{name: name}, label: @label, expr: 1 + 1 do
p do
"#{name}'s in the footer!"
end
@ -439,6 +446,7 @@ defmodule Temple.RendererTest do
</div>
<footer>
<span>i'm a slot attribute</span>
<p>
motchy boi's in the footer!
</p>
@ -501,5 +509,58 @@ defmodule Temple.RendererTest do
assert expected == result
end
test "multiple slots" do
assigns = %{}
result =
Renderer.compile do
div do
c &named_slot/1, name: "motchy boi" do
span do: "i'm a slot"
slot :footer, let: %{name: name} do
p do
"#{name}'s in the footer!"
end
end
slot :footer, let: %{name: name} do
p do
"#{name} is the second footer!"
end
end
end
end
end
# heex
expected = """
<div>
<div>
motchy boi is above the slot
<span>i'm a slot</span>
</div>
<footer>
<span></span>
<p>
motchy boi's in the footer!
</p>
<span></span>
<p>
motchy boi is the second footer!
</p>
</footer>
</div>
"""
assert expected == result
end
end
end