Repository: mhanberg/temple
Pull request #86 (2020-07-16 02:33:36)

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


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77a230f..c3832c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
 
 ## Master
 
+## 0.6.0-alpha.2
+
+### Component API
+
+Please see the README for more details regarding the Component API
+
 ## 0.6.0-alpha.1
 
 ### Generators
diff --git a/README.md b/README.md
index 589a242..289dafe 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,64 @@ temple do
 end
 ```
 
+### Components
+
+To define a component, you can create a file in your configured temple
+components directory, which defaults to `lib/components`. You would
+probably want to change that to be `lib/my_app_web/components` if you
+are building a phoenix app.
+
+```elixir
+# config/config.exs
+
+config :temple, :components_path, "./lib/my_app_web/components"
+```
+
+This file should be of the `.exs` extension.
+
+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 demonstrated 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
+```
+
 ### Phoenix templates
 
 Add the templating engine to your Phoenix configuration.
diff --git a/config/config.exs b/config/config.exs
index d2d855e..8233fe9 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,1 +1,3 @@
 use Mix.Config
+
+import_config "#{Mix.env()}.exs"
diff --git a/config/dev.exs b/config/dev.exs
new file mode 100644
index 0000000..d2d855e
--- /dev/null
+++ b/config/dev.exs
@@ -0,0 +1,1 @@
+use Mix.Config
diff --git a/lib/mix/tasks/temple.gen.live.ex b/lib/mix/tasks/temple.gen.live.ex
index 031e4ad..c68cb46 100644
--- a/lib/mix/tasks/temple.gen.live.ex
+++ b/lib/mix/tasks/temple.gen.live.ex
@@ -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
diff --git a/lib/temple.ex b/lib/temple.ex
index 6a09900..74be0a3 100644
--- a/lib/temple.ex
+++ b/lib/temple.ex
@@ -122,266 +122,11 @@ defmodule Temple do
     end
   end
 
-  defmodule Private do
-    @moduledoc false
-    @aliases Application.get_env(:temple, :aliases, [])
-
-    @nonvoid_elements ~w[
-    head title style script
-    noscript template
-    body section nav article aside h1 h2 h3 h4 h5 h6
-    header footer address main
-    p pre blockquote ol ul li dl dt dd figure figcaption div
-    a em strong small s cite q dfn abbr data time code var samp kbd
-    sub sup i b u mark ruby rt rp bdi bdo span
-    ins del
-    iframe object video audio canvas
-    map
-    table caption colgroup tbody thead tfoot tr td th
-    form fieldset legend label button select datalist optgroup
-    option textarea output progress meter
-    details summary menuitem menu
-    html
-  ]a
-
-    @nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
-                                Keyword.get(@aliases, el, el)
-                              end)
-    @nonvoid_elements_lookup Enum.map(@nonvoid_elements, fn el ->
-                               {Keyword.get(@aliases, el, el), el}
-                             end)
-
-    @void_elements ~w[
-    meta link base
-    area br col embed hr img input keygen param source track wbr
-  ]a
-
-    @void_elements_aliases Enum.map(@void_elements, fn el -> Keyword.get(@aliases, el, el) end)
-    @void_elements_lookup Enum.map(@void_elements, fn el ->
-                            {Keyword.get(@aliases, el, el), el}
-                          end)
-
-    def snake_to_kebab(stringable),
-      do:
-        stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
-
-    def kebab_to_snake(stringable),
-      do: stringable |> to_string() |> String.replace("-", "_")
-
-    def compile_attrs([]), do: ""
-
-    def compile_attrs([attrs]) when is_list(attrs) do
-      compile_attrs(attrs)
-    end
-
-    def compile_attrs(attrs) do
-      for {name, value} <- attrs, into: "" do
-        name = snake_to_kebab(name)
-
-        case value do
-          {_, _, _} = macro ->
-            " " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
-
-          value ->
-            " " <> name <> "=\"" <> to_string(value) <> "\""
-        end
-      end
-    end
-
-    def split_args(nil), do: {[], []}
-
-    def split_args(args) do
-      {do_and_else, args} =
-        args
-        |> Enum.split_with(fn
-          arg when is_list(arg) ->
-            (Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
-
-          _ ->
-            false
-        end)
-
-      {List.flatten(do_and_else), args}
-    end
-
-    def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do
-      split_on_fn(rest, {args, func, args2})
-    end
-
-    def split_on_fn([arg | rest], {args, nil, args2}) do
-      split_on_fn(rest, {[arg | args], nil, args2})
-    end
-
-    def split_on_fn([arg | rest], {args, func, args2}) do
-      split_on_fn(rest, {args, func, [arg | args2]})
-    end
-
-    def split_on_fn([], {args, func, args2}) do
-      {Enum.reverse(args), func, Enum.reverse(args2)}
-    end
-
-    def pop_compact?([]), do: {false, []}
-    def pop_compact?([args]) when is_list(args), do: pop_compact?(args)
-
-    def pop_compact?(args) do
-      Keyword.pop(args, :compact, false)
-    end
-
-    def traverse(buffer, {:__block__, _meta, block}) do
-      traverse(buffer, block)
-    end
-
-    def traverse(buffer, {name, meta, args} = macro) do
-      {do_and_else, args} =
-        args
-        |> split_args()
-
-      includes_fn? = args |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
-
-      case name do
-        {:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @nonvoid_elements_aliases ->
-          {do_and_else, args} =
-            case args do
-              [args] ->
-                {do_value, args} = Keyword.pop(args, :do)
-
-                do_and_else = Keyword.put_new(do_and_else, :do, do_value)
-
-                {do_and_else, args}
-
-              _ ->
-                {do_and_else, args}
-            end
-
-          name = @nonvoid_elements_lookup[name]
-
-          {compact?, args} = pop_compact?(args)
-
-          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
-          unless compact?, do: Buffer.put(buffer, "\n")
-          traverse(buffer, do_and_else[:do])
-          if compact?, do: Buffer.remove_new_line(buffer)
-          Buffer.put(buffer, "</#{name}>")
-          Buffer.put(buffer, "\n")
-
-        {:., _, [{:__aliases__, _, [:Temple]}, name]} when name in @void_elements_aliases ->
-          name = @void_elements_lookup[name]
-
-          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
-          Buffer.put(buffer, "\n")
-
-        name when name in @nonvoid_elements_aliases ->
-          {do_and_else, args} =
-            case args do
-              [args] ->
-                {do_value, args} = Keyword.pop(args, :do)
-
-                do_and_else = Keyword.put_new(do_and_else, :do, do_value)
-
-                {do_and_else, args}
-
-              _ ->
-                {do_and_else, args}
-            end
-
-          name = @nonvoid_elements_lookup[name]
-
-          {compact?, args} = pop_compact?(args)
-
-          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
-          unless compact?, do: Buffer.put(buffer, "\n")
-          traverse(buffer, do_and_else[:do])
-          if compact?, do: Buffer.remove_new_line(buffer)
-          Buffer.put(buffer, "</#{name}>")
-          Buffer.put(buffer, "\n")
-
-        name when name in @void_elements_aliases ->
-          name = @void_elements_lookup[name]
-
-          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
-          Buffer.put(buffer, "\n")
-
-        name when includes_fn? ->
-          {args, func_arg, args2} = split_on_fn(args, {[], nil, []})
-
-          {func, _, [{arrow, _, [[{arg, _, _}], block]}]} = func_arg
-
-          Buffer.put(
-            buffer,
-            "<%= " <>
-              to_string(name) <>
-              " " <>
-              (Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", ")) <>
-              ", " <>
-              to_string(func) <> " " <> to_string(arg) <> " " <> to_string(arrow) <> " %>"
-          )
-
-          Buffer.put(buffer, "\n")
-
-          traverse(buffer, block)
-
-          if Enum.any?(args2) do
-            Buffer.put(
-              buffer,
-              "<% end, " <>
-                (Enum.map(args2, fn arg -> Macro.to_string(arg) end)
-                 |> Enum.join(", ")) <> " %>"
-            )
-
-            Buffer.put(buffer, "\n")
-          else
-            Buffer.put(buffer, "<% end %>")
-            Buffer.put(buffer, "\n")
-          end
-
-        name when name in [:for, :if, :unless] ->
-          Buffer.put(buffer, "<%= " <> Macro.to_string({name, meta, args}) <> " do %>")
-          Buffer.put(buffer, "\n")
-
-          traverse(buffer, do_and_else[:do])
-
-          if Keyword.has_key?(do_and_else, :else) do
-            Buffer.put(buffer, "<% else %>")
-            Buffer.put(buffer, "\n")
-            traverse(buffer, do_and_else[:else])
-          end
-
-          Buffer.put(buffer, "<% end %>")
-          Buffer.put(buffer, "\n")
-
-        name when name in [:=] ->
-          Buffer.put(buffer, "<% " <> Macro.to_string(macro) <> " %>")
-          Buffer.put(buffer, "\n")
-          traverse(buffer, do_and_else[:do])
-
-        _ ->
-          Buffer.put(buffer, "<%= " <> Macro.to_string(macro) <> " %>")
-          Buffer.put(buffer, "\n")
-          traverse(buffer, do_and_else[:do])
-      end
-    end
-
-    def traverse(buffer, [first | rest]) do
-      traverse(buffer, first)
-
-      traverse(buffer, rest)
-    end
-
-    def traverse(buffer, text) when is_binary(text) do
-      Buffer.put(buffer, text)
-      Buffer.put(buffer, "\n")
-    end
-
-    def traverse(_buffer, arg) when arg in [nil, []] do
-      nil
-    end
-  end
-
   defmacro temple([do: block] = _block) do
     {:ok, buffer} = Buffer.start_link()
 
     buffer
-    |> Temple.Private.traverse(block)
+    |> Temple.Parser.Private.traverse(block)
 
     markup = Buffer.get(buffer)
 
@@ -399,7 +144,7 @@ defmodule Temple do
       {:ok, buffer} = Buffer.start_link()
 
       buffer
-      |> Temple.Private.traverse(unquote(block))
+      |> Temple.Parser.Private.traverse(unquote(block))
 
       markup = Buffer.get(buffer)
 
@@ -413,7 +158,7 @@ defmodule Temple do
     {:ok, buffer} = Buffer.start_link()
 
     buffer
-    |> Temple.Private.traverse(block)
+    |> Temple.Parser.Private.traverse(block)
 
     markup = Buffer.get(buffer)
 
diff --git a/lib/temple/parser.ex b/lib/temple/parser.ex
new file mode 100644
index 0000000..babaf4d
--- /dev/null
+++ b/lib/temple/parser.ex
@@ -0,0 +1,455 @@
+defmodule Temple.Parser do
+  alias Temple.Buffer
+  @components_path Application.get_env(:temple, :components_path, "./lib/components")
+
+  @aliases Application.get_env(:temple, :aliases, [])
+
+  @nonvoid_elements ~w[
+    head title style script
+    noscript template
+    body section nav article aside h1 h2 h3 h4 h5 h6
+    header footer address main
+    p pre blockquote ol ul li dl dt dd figure figcaption div
+    a em strong small s cite q dfn abbr data time code var samp kbd
+    sub sup i b u mark ruby rt rp bdi bdo span
+    ins del
+    iframe object video audio canvas
+    map
+    table caption colgroup tbody thead tfoot tr td th
+    form fieldset legend label button select datalist optgroup
+    option textarea output progress meter
+    details summary menuitem menu
+    html
+  ]a
+
+  @nonvoid_elements_aliases Enum.map(@nonvoid_elements, fn el ->
+                              Keyword.get(@aliases, el, el)
+                            end)
+  @nonvoid_elements_lookup Enum.map(@nonvoid_elements, fn el ->
+                             {Keyword.get(@aliases, el, el), el}
+                           end)
+
+  @void_elements ~w[
+    meta link base
+    area br col embed hr img input keygen param source track wbr
+  ]a
+
+  @void_elements_aliases Enum.map(@void_elements, fn el -> Keyword.get(@aliases, el, el) end)
+  @void_elements_lookup Enum.map(@void_elements, fn el ->
+                          {Keyword.get(@aliases, el, el), el}
+                        end)
+
+  defmodule Private do
+    @moduledoc false
+
+    def snake_to_kebab(stringable),
+      do:
+        stringable |> to_string() |> String.replace_trailing("_", "") |> String.replace("_", "-")
+
+    def kebab_to_snake(stringable),
+      do: stringable |> to_string() |> String.replace("-", "_")
+
+    def compile_attrs([]), do: ""
+
+    def compile_attrs([attrs]) when is_list(attrs) do
+      compile_attrs(attrs)
+    end
+
+    def compile_attrs(attrs) do
+      for {name, value} <- attrs, into: "" do
+        name = snake_to_kebab(name)
+
+        case value do
+          {_, _, _} = macro ->
+            " " <> name <> "=\"<%= " <> Macro.to_string(macro) <> " %>\""
+
+          value ->
+            " " <> name <> "=\"" <> to_string(value) <> "\""
+        end
+      end
+    end
+
+    def split_args(nil), do: {[], []}
+
+    def split_args(args) do
+      {do_and_else, args} =
+        args
+        |> Enum.split_with(fn
+          arg when is_list(arg) ->
+            (Keyword.keys(arg) -- [:do, :else]) |> Enum.count() == 0
+
+          _ ->
+            false
+        end)
+
+      {List.flatten(do_and_else), args}
+    end
+
+    def split_on_fn([{:fn, _, _} = func | rest], {args, _, args2}) do
+      split_on_fn(rest, {args, func, args2})
+    end
+
+    def split_on_fn([arg | rest], {args, nil, args2}) do
+      split_on_fn(rest, {[arg | args], nil, args2})
+    end
+
+    def split_on_fn([arg | rest], {args, func, args2}) do
+      split_on_fn(rest, {args, func, [arg | args2]})
+    end
+
+    def split_on_fn([], {args, func, args2}) do
+      {Enum.reverse(args), func, Enum.reverse(args2)}
+    end
+
+    def pop_compact?([]), do: {false, []}
+    def pop_compact?([args]) when is_list(args), do: pop_compact?(args)
+
+    def pop_compact?(args) do
+      Keyword.pop(args, :compact, false)
+    end
+
+    def traverse(buffer, {:__block__, _meta, block}) do
+      traverse(buffer, block)
+    end
+
+    def traverse(buffer, {_name, _meta, _args} = original_macro) do
+      Temple.Parser.parsers()
+      |> Enum.reduce_while(original_macro, fn parser, macro ->
+        with true <- parser.applicable?.(macro),
+             :ok <- parser.parse.(macro, buffer) do
+          {:halt, macro}
+        else
+          {:component_applied, adjusted_macro} ->
+            traverse(buffer, adjusted_macro)
+
+            {:halt, adjusted_macro}
+
+          false ->
+            {:cont, macro}
+        end
+      end)
+    end
+
+    def traverse(buffer, [first | rest]) do
+      traverse(buffer, first)
+
+      traverse(buffer, rest)
+    end
+
+    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
+      :ok
+    end
+  end
+
+  def parsers(),
+    do: [
+      %{
+        name: :temple_namespace_nonvoid,
+        applicable?: fn {name, _meta, _args} ->
+          try do
+            {:., _, [{:__aliases__, _, [:Temple]}, name]} = name
+            name in @nonvoid_elements_aliases
+          rescue
+            MatchError ->
+              false
+          end
+        end,
+        parse: fn {name, _meta, args}, buffer ->
+          import Temple.Parser.Private
+          {:., _, [{:__aliases__, _, [:Temple]}, name]} = name
+
+          {do_and_else, args} =
+            args
+            |> split_args()
+
+          {do_and_else, args} =
+            case args do
+              [args] ->
+                {do_value, args} = Keyword.pop(args, :do)
+
+                do_and_else = Keyword.put_new(do_and_else, :do, do_value)
+
+                {do_and_else, args}
+
+              _ ->
+                {do_and_else, args}
+            end
+
+          name = @nonvoid_elements_lookup[name]
+
+          {compact?, args} = pop_compact?(args)
+
+          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
+          unless compact?, do: Buffer.put(buffer, "\n")
+          traverse(buffer, do_and_else[:do])
+          if compact?, do: Buffer.remove_new_line(buffer)
+          Buffer.put(buffer, "</#{name}>")
+          Buffer.put(buffer, "\n")
+        end
+      },
+      %{
+        name: :temple_namespace_void,
+        applicable?: fn {name, _meta, _args} ->
+          try do
+            {:., _, [{:__aliases__, _, [:Temple]}, name]} = name
+            name in @void_elements_aliases
+          rescue
+            MatchError ->
+              false
+          end
+        end,
+        parse: fn {name, _, args}, buffer ->
+          import Temple.Parser.Private
+          {:., _, [{:__aliases__, _, [:Temple]}, name]} = name
+
+          {_do_and_else, args} =
+            args
+            |> split_args()
+
+          name = @void_elements_lookup[name]
+
+          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
+          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, _, _} ->
+          name in @nonvoid_elements_aliases
+        end,
+        parse: fn {name, _, args}, buffer ->
+          import Temple.Parser.Private
+
+          {do_and_else, args} =
+            args
+            |> split_args()
+
+          {do_and_else, args} =
+            case args do
+              [args] ->
+                {do_value, args} = Keyword.pop(args, :do)
+
+                do_and_else = Keyword.put_new(do_and_else, :do, do_value)
+
+                {do_and_else, args}
+
+              _ ->
+                {do_and_else, args}
+            end
+
+          name = @nonvoid_elements_lookup[name]
+
+          {compact?, args} = pop_compact?(args)
+
+          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
+          unless compact?, do: Buffer.put(buffer, "\n")
+          traverse(buffer, do_and_else[:do])
+          if compact?, do: Buffer.remove_new_line(buffer)
+          Buffer.put(buffer, "</#{name}>")
+          Buffer.put(buffer, "\n")
+        end
+      },
+      %{
+        name: :void_elements_aliases,
+        applicable?: fn {name, _, _} ->
+          name in @void_elements_aliases
+        end,
+        parse: fn {name, _, args}, buffer ->
+          import Temple.Parser.Private
+
+          {_do_and_else, args} =
+            args
+            |> split_args()
+
+          name = @void_elements_lookup[name]
+
+          Buffer.put(buffer, "<#{name}#{compile_attrs(args)}>")
+          Buffer.put(buffer, "\n")
+        end
+      },
+      %{
+        name: :anonymous_functions,
+        applicable?: fn {_, _, args} ->
+          import Temple.Parser.Private, only: [split_args: 1]
+
+          args |> split_args() |> elem(1) |> Enum.any?(fn x -> match?({:fn, _, _}, x) end)
+        end,
+        parse: fn {name, _, args}, buffer ->
+          import Temple.Parser.Private
+
+          {_do_and_else, args} =
+            args
+            |> split_args()
+
+          {args, func_arg, args2} = split_on_fn(args, {[], nil, []})
+
+          {func, _, [{arrow, _, [[{arg, _, _}], block]}]} = func_arg
+
+          Buffer.put(
+            buffer,
+            "<%= " <>
+              to_string(name) <>
+              " " <>
+              (Enum.map(args, &Macro.to_string(&1)) |> Enum.join(", ")) <>
+              ", " <>
+              to_string(func) <> " " <> to_string(arg) <> " " <> to_string(arrow) <> " %>"
+          )
+
+          Buffer.put(buffer, "\n")
+
+          traverse(buffer, block)
+
+          if Enum.any?(args2) do
+            Buffer.put(
+              buffer,
+              "<% end, " <>
+                (Enum.map(args2, fn arg -> Macro.to_string(arg) end)
+                 |> Enum.join(", ")) <> " %>"
+            )
+
+            Buffer.put(buffer, "\n")
+          else
+            Buffer.put(buffer, "<% end %>")
+            Buffer.put(buffer, "\n")
+          end
+        end
+      },
+      %{
+        name: :for_if_unless,
+        applicable?: fn {name, _, _} ->
+          name in [:for, :if, :unless]
+        end,
+        parse: fn {name, meta, args}, buffer ->
+          import Temple.Parser.Private
+
+          {do_and_else, args} =
+            args
+            |> split_args()
+
+          Buffer.put(buffer, "<%= " <> Macro.to_string({name, meta, args}) <> " do %>")
+          Buffer.put(buffer, "\n")
+
+          traverse(buffer, do_and_else[:do])
+
+          if Keyword.has_key?(do_and_else, :else) do
+            Buffer.put(buffer, "<% else %>")
+            Buffer.put(buffer, "\n")
+            traverse(buffer, do_and_else[:else])
+          end
+
+          Buffer.put(buffer, "<% end %>")
+          Buffer.put(buffer, "\n")
+        end
+      },
+      %{
+        name: :match,
+        applicable?: fn {name, _, _} ->
+          name in [:=]
+        end,
+        parse: fn {_, _, args} = macro, buffer ->
+          import Temple.Parser.Private
+
+          {do_and_else, _args} =
+            args
+            |> split_args()
+
+          Buffer.put(buffer, "<% " <> Macro.to_string(macro) <> " %>")
+          Buffer.put(buffer, "\n")
+          traverse(buffer, do_and_else[:do])
+        end
+      },
+      %{
+        name: :default,
+        applicable?: fn _ -> true end,
+        parse: fn {_, _, args} = macro, buffer ->
+          import Temple.Parser.Private
+
+          {do_and_else, _args} =
+            args
+            |> split_args()
+
+          Buffer.put(buffer, "<%= " <> Macro.to_string(macro) <> " %>")
+          Buffer.put(buffer, "\n")
+          traverse(buffer, do_and_else[:do])
+        end
+      }
+    ]
+end
diff --git a/lib/temple/recompiler.ex b/lib/temple/recompiler.ex
new file mode 100644
index 0000000..3a2bff0
--- /dev/null
+++ b/lib/temple/recompiler.ex
@@ -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


Test patch:
diff --git a/config/test.exs b/config/test.exs
new file mode 100644
index 0000000..ca31719
--- /dev/null
+++ b/config/test.exs
@@ -0,0 +1,3 @@
+use Mix.Config
+
+config :temple, components_path: "./test/support/components"
diff --git a/test/partial_test.exs b/test/partial_test.exs
new file mode 100644
index 0000000..b7196a8
--- /dev/null
+++ b/test/partial_test.exs
@@ -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
diff --git a/test/support/components/component.exs b/test/support/components/component.exs
new file mode 100644
index 0000000..1b05689
--- /dev/null
+++ b/test/support/components/component.exs
@@ -0,0 +1,3 @@
+div class: @assign do
+  @children
+end
diff --git a/test/support/components/component2.exs b/test/support/components/component2.exs
new file mode 100644
index 0000000..bca867a
--- /dev/null
+++ b/test/support/components/component2.exs
@@ -0,0 +1,3 @@
+div class: @class do
+  @children
+end
diff --git a/test/support/components/has_temple.exs b/test/support/components/has_temple.exs
new file mode 100644
index 0000000..41abb12
--- /dev/null
+++ b/test/support/components/has_temple.exs
@@ -0,0 +1,3 @@
+div class: @temple[:class] do
+  @children
+end
diff --git a/test/support/components/inner.exs b/test/support/components/inner.exs
new file mode 100644
index 0000000..83dfb5d
--- /dev/null
+++ b/test/support/components/inner.exs
@@ -0,0 +1,3 @@
+div id: "inner", outer_id: @outer_id do
+  @children
+end
diff --git a/test/support/components/outer.exs b/test/support/components/outer.exs
new file mode 100644
index 0000000..eee0b80
--- /dev/null
+++ b/test/support/components/outer.exs
@@ -0,0 +1,3 @@
+inner outer_id: "from-outer" do
+  @children
+end
diff --git a/test/support/components/section.exs b/test/support/components/section.exs
new file mode 100644
index 0000000..bbd92d0
--- /dev/null
+++ b/test/support/components/section.exs
@@ -0,0 +1,3 @@
+section class: "foo!" do
+  @children
+end
diff --git a/test/temple_test.exs b/test/temple_test.exs
index fbac550..3544b32 100644
--- a/test/temple_test.exs
+++ b/test/temple_test.exs
@@ -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