Repository: mhanberg/temple
Pull request #91 (2020-07-25 00:37:49)

Module based Component API


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 06c8163..c742cd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Master
 
+### Breaking
+
+Components are now defined using modules. You can convert your existing components by configuring your component prefix and wrapping your current component files in the `Temple.Component` behaviour implementation.
+
 ### Bugs
 
 - Did not correctly parse expressions with do blocks where the expression had two or more arguments before the block
diff --git a/README.md b/README.md
index 070774e..ff08310 100644
--- a/README.md
+++ b/README.md
@@ -58,28 +58,32 @@ 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.
+To define a component, you can define a module that that starts with your defined components prefix. The last name in the module should be a came-cases version of the component name.
+
+This module should implement the `Temple.Component` behaviour.
 
 ```elixir
 # config/config.exs
 
-config :temple, :components_path, "./lib/my_app_web/components"
+config :temple, :components_prefix, MyAppWeb.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.
+For example, if I were to define a `flex` component, I would create the following module.
 
 ```elixir
-div class: "flex #{@temple[:class]}", id: @id do
-  @children
+defmodule MyAppWeb.Components.Flex do
+  @behavior Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      div class: "flex #{@temple[:class]}", id: @id do
+        @children
+      end
+    end
+  end
 end
 ```
 
diff --git a/config/config.exs b/config/config.exs
index 8233fe9..e3dfb65 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1,3 +1,4 @@
 use Mix.Config
 
+config :temple, :component_prefix, Temple.Components
 import_config "#{Mix.env()}.exs"
diff --git a/lib/temple/component.ex b/lib/temple/component.ex
new file mode 100644
index 0000000..d852d71
--- /dev/null
+++ b/lib/temple/component.ex
@@ -0,0 +1,23 @@
+defmodule Temple.Component do
+  @moduledoc """
+  Behaviour for defining temple components.
+  """
+
+  @doc """
+  The render callback must return AST to be inserted into the markup. 
+
+  Since you need to return AST, it is typical to wrap the contents of the function in a `quote` block like:
+
+  ```
+  @impl Temple.Component
+  def render() do
+    quote do
+      div do
+        @children
+      end
+    end
+  end
+  ```
+  """
+  @callback render() :: Macro.t()
+end
diff --git a/lib/temple/parsers/components.ex b/lib/temple/parsers/components.ex
index f262f2a..cf6137d 100644
--- a/lib/temple/parsers/components.ex
+++ b/lib/temple/parsers/components.ex
@@ -1,15 +1,20 @@
 defmodule Temple.Parser.Components do
   @behaviour Temple.Parser
-  @components_path Application.get_env(:temple, :components_path, "./lib/components")
+  @component_prefix Application.fetch_env!(:temple, :component_prefix)
 
   alias Temple.Parser
 
   def applicable?({name, meta, _}) when is_atom(name) do
-    !meta[:temple_component_applied] && File.exists?(Path.join([@components_path, "#{name}.exs"]))
+    !meta[:temple_component_applied] &&
+      match?({:module, _}, name |> component_module() |> Code.ensure_compiled())
   end
 
   def applicable?(_), do: false
 
+  defp component_module(name) do
+    Module.concat([@component_prefix, Macro.camelize(to_string(name))])
+  end
+
   def run({name, _meta, args}, _buffer) do
     {assigns, children} =
       case args do
@@ -26,9 +31,9 @@ defmodule Temple.Parser.Components do
           {nil, nil}
       end
 
-    ast =
-      File.read!(Path.join([@components_path, "#{name}.exs"]))
-      |> Code.string_to_quoted!()
+    component_module = Module.concat([@component_prefix, Macro.camelize(to_string(name))])
+
+    ast = apply(component_module, :render, [])
 
     {name, meta, args} =
       ast


Test patch:
diff --git a/integration_test/temple_demo/config/config.exs b/integration_test/temple_demo/config/config.exs
index c4db027..2bbad10 100644
--- a/integration_test/temple_demo/config/config.exs
+++ b/integration_test/temple_demo/config/config.exs
@@ -12,8 +12,7 @@ config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
 config :temple_demo,
   ecto_repos: [TempleDemo.Repo]
 
-config :phoenix, :template_engines,
-  exs: Temple.Engine
+config :phoenix, :template_engines, exs: Temple.Engine
 
 # Configures the endpoint
 config :temple_demo, TempleDemoWeb.Endpoint,
@@ -31,11 +30,13 @@ config :logger, :console,
 # Use Jason for JSON parsing in Phoenix
 config :phoenix, :json_library, Jason
 
-config :temple, :aliases,
-  label: :_label,
-  link: :_link,
-  textarea: :_textarea
-
+config :temple,
+  aliases: [
+    label: :_label,
+    link: :_link,
+    textarea: :_textarea
+  ],
+  component_prefix: TempleDemoWeb.Component
 
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
diff --git a/test/support/components/component.ex b/test/support/components/component.ex
new file mode 100644
index 0000000..5db2637
--- /dev/null
+++ b/test/support/components/component.ex
@@ -0,0 +1,12 @@
+defmodule Temple.Components.Component do
+  @behaviour Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      div class: @assign do
+        @children
+      end
+    end
+  end
+end
diff --git a/test/support/components/component.exs b/test/support/components/component.exs
deleted file mode 100644
index 1b05689..0000000
--- a/test/support/components/component.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-div class: @assign do
-  @children
-end
diff --git a/test/support/components/component2.ex b/test/support/components/component2.ex
new file mode 100644
index 0000000..6b81d58
--- /dev/null
+++ b/test/support/components/component2.ex
@@ -0,0 +1,12 @@
+defmodule Temple.Components.Component2 do
+  @behaviour Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      div class: @class do
+        @children
+      end
+    end
+  end
+end
diff --git a/test/support/components/component2.exs b/test/support/components/component2.exs
deleted file mode 100644
index bca867a..0000000
--- a/test/support/components/component2.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-div class: @class do
-  @children
-end
diff --git a/test/support/components/has_temple.ex b/test/support/components/has_temple.ex
new file mode 100644
index 0000000..93dced4
--- /dev/null
+++ b/test/support/components/has_temple.ex
@@ -0,0 +1,12 @@
+defmodule Temple.Components.HasTemple do
+  @behaviour Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      div class: @temple[:class] do
+        @children
+      end
+    end
+  end
+end
diff --git a/test/support/components/has_temple.exs b/test/support/components/has_temple.exs
deleted file mode 100644
index 41abb12..0000000
--- a/test/support/components/has_temple.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-div class: @temple[:class] do
-  @children
-end
diff --git a/test/support/components/inner.ex b/test/support/components/inner.ex
new file mode 100644
index 0000000..9ca399e
--- /dev/null
+++ b/test/support/components/inner.ex
@@ -0,0 +1,12 @@
+defmodule Temple.Components.Inner do
+  @behaviour Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      div id: "inner", outer_id: @outer_id do
+        @children
+      end
+    end
+  end
+end
diff --git a/test/support/components/inner.exs b/test/support/components/inner.exs
deleted file mode 100644
index 83dfb5d..0000000
--- a/test/support/components/inner.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-div id: "inner", outer_id: @outer_id do
-  @children
-end
diff --git a/test/support/components/outer.ex b/test/support/components/outer.ex
new file mode 100644
index 0000000..f7b530c
--- /dev/null
+++ b/test/support/components/outer.ex
@@ -0,0 +1,12 @@
+defmodule Temple.Components.Outer do
+  @behaviour Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      inner outer_id: "from-outer" do
+        @children
+      end
+    end
+  end
+end
diff --git a/test/support/components/outer.exs b/test/support/components/outer.exs
deleted file mode 100644
index eee0b80..0000000
--- a/test/support/components/outer.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-inner outer_id: "from-outer" do
-  @children
-end
diff --git a/test/support/components/section.ex b/test/support/components/section.ex
new file mode 100644
index 0000000..06f8bc2
--- /dev/null
+++ b/test/support/components/section.ex
@@ -0,0 +1,12 @@
+defmodule Temple.Components.Section do
+  @behaviour Temple.Component
+
+  @impl Temple.Component
+  def render do
+    quote do
+      section class: "foo!" do
+        @children
+      end
+    end
+  end
+end
diff --git a/test/support/components/section.exs b/test/support/components/section.exs
deleted file mode 100644
index bbd92d0..0000000
--- a/test/support/components/section.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-section class: "foo!" do
-  @children
-end