Components API
Components work very similarly to how they worked before, but with a few differences. To define a component, you can create a file in your configured temple components directory, which defaults to `lib/components`. You would probably want ot change that to be `lib/my_app_web/components` if you are building a phoenix app. This file should be of the `.exs` extension, and contain any temple compatible code. You can then use this component in any other temple template. For example, if I were to define a `flex` component, I would create a file called `lib/my_app_web/components/flex.exs`, with the following contents. ```elixir div class: "flex #{@temple[:class]}", id: @id do @children end ``` And we could use the component like so ```elixir flex class: "justify-between items-center", id: "arnold" do div do: "Hi" div do: "I'm" div do: "Arnold" div do: "Schwarzenegger" end ``` We've demonstated several features to components in this example. We can pass assigns to our component, and access them just like we would in a normal phoenix template. If they don't match up with any assigns we passed to our component, they will be rendered as-is, and will become a normal Phoenix assign. You can also access a special `@temple` assign. This allows you do optionally pass an assign, and not have the `@my_assign` pass through. If you didn't pass it to your component, it will evaluate to nil. The block passed to your component can be accessed as `@children`. This allows your components to wrap a body of markup from the call site. In order for components to trigger a recompile when they are changed, you can call `use Temple.Recompiler` in your `lib/my_app_web.ex` file, in the `view`, `live_view`, and `live_component` functions ```elixir def view do quote do # ... use Temple.Recompiler # ... end end ```
This commit is contained in:
parent
f8f1ec623f
commit
1a5837d1b7
14 changed files with 221 additions and 9 deletions
|
@ -1 +1,3 @@
|
|||
use Mix.Config
|
||||
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
1
config/dev.exs
Normal file
1
config/dev.exs
Normal file
|
@ -0,0 +1 @@
|
|||
use Mix.Config
|
3
config/test.exs
Normal file
3
config/test.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
use Mix.Config
|
||||
|
||||
config :temple, components_path: "./test/support/components"
|
|
@ -152,10 +152,10 @@ if Code.ensure_loaded?(Mix.Phoenix) do
|
|||
end
|
||||
|
||||
defp copy_new_files(%Context{} = context, binding, paths) do
|
||||
files = files_to_be_generated(context) |> IO.inspect(label: "FILES")
|
||||
files = files_to_be_generated(context)
|
||||
|
||||
Mix.Phoenix.copy_from(
|
||||
paths |> IO.inspect(label: "PATHS"),
|
||||
paths,
|
||||
"priv/templates/temple.gen.live",
|
||||
binding,
|
||||
files
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Temple.Parser do
|
||||
alias Temple.Buffer
|
||||
@components_path Application.get_env(:temple, :components_path, "./lib/components")
|
||||
|
||||
@aliases Application.get_env(:temple, :aliases, [])
|
||||
|
||||
|
@ -111,14 +112,20 @@ defmodule Temple.Parser do
|
|||
traverse(buffer, block)
|
||||
end
|
||||
|
||||
def traverse(buffer, {_name, _meta, _args} = macro) do
|
||||
def traverse(buffer, {_name, _meta, _args} = original_macro) do
|
||||
Temple.Parser.parsers()
|
||||
|> Enum.reduce_while(nil, fn parser, _ ->
|
||||
if parser.applicable?.(macro) do
|
||||
parser.parse.(macro, buffer)
|
||||
{:halt, nil}
|
||||
|> Enum.reduce_while(original_macro, fn parser, macro ->
|
||||
with true <- parser.applicable?.(macro),
|
||||
:ok <- parser.parse.(macro, buffer) do
|
||||
{:halt, macro}
|
||||
else
|
||||
{:cont, nil}
|
||||
{:component_applied, adjusted_macro} ->
|
||||
traverse(buffer, adjusted_macro)
|
||||
|
||||
{:halt, adjusted_macro}
|
||||
|
||||
false ->
|
||||
{:cont, macro}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
@ -132,10 +139,12 @@ defmodule Temple.Parser do
|
|||
def traverse(buffer, text) when is_binary(text) do
|
||||
Buffer.put(buffer, text)
|
||||
Buffer.put(buffer, "\n")
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def traverse(_buffer, arg) when arg in [nil, []] do
|
||||
nil
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -210,6 +219,77 @@ defmodule Temple.Parser do
|
|||
Buffer.put(buffer, "\n")
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :components,
|
||||
applicable?: fn {name, meta, _} ->
|
||||
try do
|
||||
!meta[:temple_component_applied] &&
|
||||
File.exists?(Path.join([@components_path, "#{name}.exs"]))
|
||||
rescue
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end,
|
||||
parse: fn {name, _meta, args}, _buffer ->
|
||||
import Temple.Parser.Private
|
||||
|
||||
{assigns, children} =
|
||||
case args do
|
||||
[assigns, [do: block]] ->
|
||||
{assigns, block}
|
||||
|
||||
[[do: block]] ->
|
||||
{nil, block}
|
||||
|
||||
[assigns] ->
|
||||
{assigns, nil}
|
||||
|
||||
_ ->
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
ast =
|
||||
File.read!(Path.join([@components_path, "#{name}.exs"]))
|
||||
|> Code.string_to_quoted!()
|
||||
|
||||
{name, meta, args} =
|
||||
ast
|
||||
|> Macro.prewalk(fn
|
||||
{:@, _, [{:children, _, _}]} ->
|
||||
children
|
||||
|
||||
{:@, _, [{:temple, _, _}]} ->
|
||||
assigns
|
||||
|
||||
{:@, _, [{name, _, _}]} = node ->
|
||||
if !is_nil(assigns) && name in Keyword.keys(assigns) do
|
||||
Keyword.get(assigns, name, nil)
|
||||
else
|
||||
node
|
||||
end
|
||||
|
||||
node ->
|
||||
node
|
||||
end)
|
||||
|
||||
ast =
|
||||
if Enum.any?(
|
||||
[
|
||||
@nonvoid_elements,
|
||||
@nonvoid_elements_aliases,
|
||||
@void_elements,
|
||||
@void_elements_aliases
|
||||
],
|
||||
fn elements -> name in elements end
|
||||
) do
|
||||
{name, Keyword.put(meta, :temple_component_applied, true), args}
|
||||
else
|
||||
{name, meta, args}
|
||||
end
|
||||
|
||||
{:component_applied, ast}
|
||||
end
|
||||
},
|
||||
%{
|
||||
name: :nonvoid_elements_aliases,
|
||||
applicable?: fn {name, _, _} ->
|
||||
|
|
15
lib/temple/recompiler.ex
Normal file
15
lib/temple/recompiler.ex
Normal file
|
@ -0,0 +1,15 @@
|
|||
defmodule Temple.Recompiler do
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
component_path = Application.get_env(:temple, :components_path)
|
||||
|
||||
for f <- File.ls!(component_path),
|
||||
do:
|
||||
Module.put_attribute(
|
||||
__MODULE__,
|
||||
:external_resource,
|
||||
Path.join(component_path, f)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
16
test/partial_test.exs
Normal file
16
test/partial_test.exs
Normal file
|
@ -0,0 +1,16 @@
|
|||
defmodule PartialTest do
|
||||
use ExUnit.Case, async: true
|
||||
use Temple
|
||||
use Temple.Support.Utils
|
||||
|
||||
test "can correctly redefine elements" do
|
||||
result =
|
||||
temple do
|
||||
section do
|
||||
"Howdy!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result == ~s{<section class="foo!">Howdy!</section>}
|
||||
end
|
||||
end
|
3
test/support/components/component.exs
Normal file
3
test/support/components/component.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div class: @assign do
|
||||
@children
|
||||
end
|
3
test/support/components/component2.exs
Normal file
3
test/support/components/component2.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div class: @class do
|
||||
@children
|
||||
end
|
3
test/support/components/has_temple.exs
Normal file
3
test/support/components/has_temple.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div class: @temple[:class] do
|
||||
@children
|
||||
end
|
3
test/support/components/inner.exs
Normal file
3
test/support/components/inner.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
div id: "inner", outer_id: @outer_id do
|
||||
@children
|
||||
end
|
3
test/support/components/outer.exs
Normal file
3
test/support/components/outer.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
inner outer_id: "from-outer" do
|
||||
@children
|
||||
end
|
3
test/support/components/section.exs
Normal file
3
test/support/components/section.exs
Normal file
|
@ -0,0 +1,3 @@
|
|||
section class: "foo!" do
|
||||
@children
|
||||
end
|
|
@ -265,4 +265,81 @@ defmodule TempleTest do
|
|||
|
||||
assert result == ~s{<p>Bob</p>\n<p><%= foo %></p>}
|
||||
end
|
||||
|
||||
test "inlines function components" do
|
||||
result =
|
||||
temple do
|
||||
div class: "font-bold" do
|
||||
"Hello, world"
|
||||
end
|
||||
|
||||
component do
|
||||
"I'm a component!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div class="font-bold">Hello, world</div><div class="<%= @assign %>">I'm a component!</div>}
|
||||
end
|
||||
|
||||
test "function components can accept local assigns" do
|
||||
result =
|
||||
temple do
|
||||
div class: "font-bold" do
|
||||
"Hello, world"
|
||||
end
|
||||
|
||||
component2 class: "bg-red" do
|
||||
"I'm a component!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div class="font-bold">Hello, world</div><div class="bg-red">I'm a component!</div>}
|
||||
end
|
||||
|
||||
test "function components can accept local assigns that are variables" do
|
||||
result =
|
||||
temple do
|
||||
div class: "font-bold" do
|
||||
"Hello, world"
|
||||
end
|
||||
|
||||
class = "bg-red"
|
||||
|
||||
component2 class: class do
|
||||
"I'm a component!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div class="font-bold">Hello, world</div><% class = "bg-red" %><div class="<%= class %>">I'm a component!</div>}
|
||||
end
|
||||
|
||||
test "function components can use other components" do
|
||||
result =
|
||||
temple do
|
||||
outer do
|
||||
"outer!"
|
||||
end
|
||||
|
||||
inner do
|
||||
"inner!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result ==
|
||||
~s{<div id="inner" outer-id="from-outer">outer!</div><div id="inner" outer-id="<%= @outer_id %>">inner!</div>}
|
||||
end
|
||||
|
||||
test "@temple should be available in any component" do
|
||||
result =
|
||||
temple do
|
||||
has_temple class: "boom" do
|
||||
"yay!"
|
||||
end
|
||||
end
|
||||
|
||||
assert result == ~s{<div class="<%= [class: "boom"][:class] %>">yay!</div>}
|
||||
end
|
||||
end
|
||||
|
|
Reference in a new issue