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:
Mitchell Hanberg 2020-07-15 22:11:35 -04:00
parent f8f1ec623f
commit 1a5837d1b7
14 changed files with 221 additions and 9 deletions

View file

@ -1 +1,3 @@
use Mix.Config
import_config "#{Mix.env()}.exs"

1
config/dev.exs Normal file
View file

@ -0,0 +1 @@
use Mix.Config

3
config/test.exs Normal file
View file

@ -0,0 +1,3 @@
use Mix.Config
config :temple, components_path: "./test/support/components"

View file

@ -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

View file

@ -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
View 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
View 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

View file

@ -0,0 +1,3 @@
div class: @assign do
@children
end

View file

@ -0,0 +1,3 @@
div class: @class do
@children
end

View file

@ -0,0 +1,3 @@
div class: @temple[:class] do
@children
end

View file

@ -0,0 +1,3 @@
div id: "inner", outer_id: @outer_id do
@children
end

View file

@ -0,0 +1,3 @@
inner outer_id: "from-outer" do
@children
end

View file

@ -0,0 +1,3 @@
section class: "foo!" do
@children
end

View file

@ -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