Module based Component API

This commit is contained in:
Mitchell Hanberg 2020-07-24 15:54:38 -04:00
parent 113a75a7eb
commit 7be82e003f
18 changed files with 134 additions and 42 deletions

View file

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

View file

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

View file

@ -1,3 +1,4 @@
use Mix.Config
config :temple, :component_prefix, Temple.Components
import_config "#{Mix.env()}.exs"

View file

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

23
lib/temple/component.ex Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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