feat: New Component API

This commit is contained in:
Mitchell Hanberg 2021-01-02 13:21:48 -05:00
parent 271567dc8f
commit ced2f6ab66
56 changed files with 715 additions and 405 deletions

View File

@ -1,5 +1,5 @@
locals_without_parens = ~w[
temple
temple c
html head title style script
noscript template
body section nav article aside h1 h2 h3 h4 h5 h6

View File

@ -30,7 +30,58 @@ end
### 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.
#### Components
Components are now a thin layer over template partials, compiling to calls to `render/3` and `render_layout/4` under the hood.
To upgrade your components the new syntax, you can copy your component markup and paste it into the `render/1` macro inside the component module and references to `@children` can be updated to `@inner_content`.
Components can are also referenced differently than before when using them. Before, one would simply call `flex` to render a component named `Flex`. Now, one must use the keyword `c` to render a component, passing the keyword the component module along with any assigns.
##### Before
```elixir
# definition
div class: "flex #{@class}" do
@children
end
# usage
flex class: "justify-between" do
for item <- @items do
div do
item.name
end
end
end
```
##### After
```elixir
# definition
defmodule MyAppWeb.Component.Flex do
use Temple.Component
render do
div class: "flex #{@class}" do
@inner_content
end
end
end
# usage
alias MyApp.Component.Flex # probably located in my_app_web.ex
c Flex, class: "justify-between" do
for item <- @items do
div do
item.name
end
end
end
```
### Bugs

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Mitchell Hanberg
Copyright (c) 2021 Mitchell Hanberg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -6,7 +6,7 @@
> You are looking at the README for the master branch. The README for the latest stable release is located [here](https://github.com/mhanberg/temple/tree/v0.5.0).
Temple is a DSL for writing HTML using Elixir.
Temple is a DSL for writing HTML and EEx using Elixir.
You're probably here because you want to use Temple to write Phoenix templates, which is why Temple includes a [Phoenix template engine](#phoenix-templates).
@ -16,7 +16,10 @@ Add `temple` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:temple, "~> 0.6.0-alpha.4"}]
[
{:temple, "~> 0.6.0-alpha.4"},
{:phoenix, ">= 1.5.0"} # requires at least Phoenix v1.5.0
]
end
```
@ -30,9 +33,9 @@ end
## Usage
Using Temple is a as simple as using the DSL inside of an `temple/1` block. This returns an EEx string at compile time.
Using Temple is as simple as using the DSL inside of an `temple/1` block. This returns an EEx string at compile time.
See the [documentation](https://hexdocs.pm/temple/Temple.Html.html) for more details.
See the [documentation](https://hexdocs.pm/temple/Temple.html) for more details.
```elixir
use Temple
@ -66,33 +69,17 @@ end
### Components
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.
Temple components are mostly a little syntax sugar over Phoenix's `render/3` and `render_layout/4` functions.
This module should implement the `Temple.Component` behaviour.
```elixir
# config/config.exs
config :temple, :component_prefix, MyAppWeb.Components
# also set the path so recompiling will work in Phoenix projects
config :temple, :components_path, "./lib/my_app_web/components"
```
You can then use this component in any other temple template.
For example, if I were to define a `flex` component, I would create the following module.
For example, if I were to define a `Flex` component, I would create the following module.
```elixir
defmodule MyAppWeb.Components.Flex do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
div class: "flex #{@temple[:class]}", id: @id do
@children
end
render do
div class: "flex #\{@class}" do
@inner_content
end
end
end
@ -101,7 +88,9 @@ end
And we could use the component like so
```elixir
flex class: "justify-between items-center", id: "arnold" do
alias MyAppWeb.Components.Flex
c Flex, class: "justify-between items-center", id: "arnold" do
div do: "Hi"
div do: "I'm"
div do: "Arnold"
@ -109,29 +98,9 @@ flex class: "justify-between items-center", id: "arnold" do
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.
Add the template engine to your Phoenix configuration.
```elixir
# config.exs

View File

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

View File

@ -1,3 +1,5 @@
use Mix.Config
config :temple, components_path: "./test/support/components"
# this is to make the warning go away,
# Temple does not use a json_library
config :phoenix, json_library: Temple

View File

@ -35,8 +35,7 @@ config :temple,
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.

View File

@ -52,7 +52,7 @@ config :temple_demo, TempleDemoWeb.Endpoint,
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/temple_demo_web/(live|views)/.*(ex)$",
~r"lib/temple_demo_web/(live|views|components)/.*(ex)$",
~r"lib/temple_demo_web/templates/.*(eex|exs)$"
]
]

View File

@ -21,6 +21,10 @@ config :temple_demo, TempleDemoWeb.Endpoint,
config :temple_demo, :sql_sandbox, true
config :wallaby,
chromedriver: [
# headless: false,
binary: System.get_env("CHROME_BROWSER")
],
base_url: "http://localhost:4002",
otp_app: :temple_demo,
screenshot_on_failure: true

View File

@ -36,6 +36,9 @@ defmodule TempleDemoWeb do
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
alias TempleDemoWeb.Component.Outer
alias TempleDemoWeb.Component.Flash
# Include shared imports and aliases for views
unquote(view_helpers())
end

View File

@ -0,0 +1,9 @@
defmodule TempleDemoWeb.Component.Flash do
use Temple.Component
render do
div class: "alert alert-#{@type}", style: "border: solid 5px pink" do
@inner_content
end
end
end

View File

@ -0,0 +1,9 @@
defmodule TempleDemoWeb.Component.Inner do
use Temple.Component
render do
div id: "inner", outer_id: @outer_id do
@inner_content
end
end
end

View File

@ -0,0 +1,10 @@
defmodule TempleDemoWeb.Component.Outer do
use Temple.Component
alias TempleDemoWeb.Component.Inner
render do
c Inner, outer_id: "from-outer" do
@inner_content
end
end
end

View File

@ -3,6 +3,10 @@ section class: "phx-hero" do
gettext("Welcome to %{name}!", name: "Phoenix")
end
c Outer, outer_id: "hello" do
"inner content of outer"
end
case @text do
"staging" ->
p do

View File

@ -1,25 +1,25 @@
form_for @changeset, @action, fn f ->
if @changeset.action do
div class: "alert alert-danger" do
c Flash, type: :info do
p do: "Oops, something went wrong! Please check the errors below."
end
end
end
label f, :title
text_input f, :title
error_tag(f, :title)
error_tag(f, :title)
label f, :body
textarea f, :body
error_tag(f, :body)
error_tag(f, :body)
label f, :published_at
datetime_select f, :published_at
error_tag(f, :published_at)
error_tag(f, :published_at)
label f, :author
text_input f, :author
error_tag(f, :author)
error_tag(f, :author)
div do
submit "Save"

View File

@ -1,27 +1,26 @@
h1 do: "Listing Posts"
table do
thead do
tr do
th do: "Title"
th do: "Body"
th do: "Published at"
th do: "Author"
th()
end
tbody do
for post <- @posts do
tr do
td do: post.title
td do: post.body
td do: post.published_at
td do: post.author
td do
link "Show", to: Routes.post_path(@conn, :show, post)
link "Edit", to: Routes.post_path(@conn, :edit, post)
link "Delete", to: Routes.post_path(@conn, :delete, post),
method: :delete, data: [confirm: "Are you sure?"]
end
c Headers do
th do: "Title"
th do: "Body"
th do: "Published at"
th do: "Author"
th do: "BOB"
end
tbody do
for post <- @posts do
tr do
td do: post.title
td do: post.body
td do: post.published_at
td do: post.author
td do
link "Show", to: Routes.post_path(@conn, :show, post)
link "Edit", to: Routes.post_path(@conn, :edit, post)
link "Delete", to: Routes.post_path(@conn, :delete, post),
method: :delete, data: [confirm: "Are you sure?"]
end
end
end

View File

@ -1,3 +1,14 @@
defmodule TempleDemoWeb.PostView do
use TempleDemoWeb, :view
import Temple.Component, only: [defcomp: 2]
def thing(), do: "foobar"
defcomp Headers do
thead id: PostView.thing() do
tr do
@inner_content
end
end
end
end

View File

@ -45,7 +45,7 @@ defmodule TempleDemo.MixProject do
{:gettext, "~> 0.11"},
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:wallaby, "~> 0.26.0", only: :test},
{:wallaby, "~> 0.28.0", only: :test},
{:tzdata, "~> 1.0.3"},
{:temple, path: "../../"}
]

View File

@ -1,42 +1,42 @@
%{
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"},
"decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
"ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mime": {:hex, :mime, "1.5.0", "203ef35ef3389aae6d361918bf3f952fa17a09e8e43b5aa592b93eba05d0fb8d", [:mix], [], "hexpm", "55a94c0f552249fc1a3dd9cd2d3ab9de9d3c89b559c2bd01121f824834f24746"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.2", "7ba05d6cb0024eefd3cb08b176e6f041a9edff094912de2f6a49e3ba67140fb3", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3047022367d415a935dceda1176e67d9c7f2d41cd52a0419b53cfca66fc4c64e"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.2.1", "13f124cf0a3ce0f1948cf24654c7b9f2347169ff75c1123f44674afee6af3b03", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "478a1bae899cac0a6e02be1deec7e2944b7754c04e7d4107fc5a517f877743c0"},
"phoenix_html": {:hex, :phoenix_html, "2.14.3", "51f720d0d543e4e157ff06b65de38e13303d5778a7919bcc696599e5934271b8", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "efd697a7fff35a13eeeb6b43db884705cba353a1a41d127d118fda5f90c8e80f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.4", "3080e8a89bab3ec08d4dd9a6858dfa24af9334464aae78c83e58a2db37c6f983", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.12.0 or ~> 0.13.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1c89595ef60f1b76ac07705e73f001823af451491792a4b0d5b2b2a3789b0a00"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.2", "38d94c30df5e2ef11000697a4fbe2b38d0fbf79239d492ff1be87bbc33bc3a84", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "a3dec3d28ddb5476c96a7c8a38ea8437923408bc88da43e5c45d97037b396280"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.13.0", "dec006b3da4ab164283d5bebe960724eb4d19cd0ed553e05fb99b260233e200f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "bd6f13b666fa9bfeca88b013db20414c693d5a5e6d19b1fc2602c282d626ed8e"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug": {:hex, :plug, "1.11.0", "f17217525597628298998bc3baed9f8ea1fa3f1160aa9871aee6df47a6e4d38e", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d9c633f0499f9dc5c2fd069161af4e2e7756890b81adcbb2ceaa074e8308876"},
"plug_cowboy": {:hex, :plug_cowboy, "2.2.1", "fcf58aa33227a4322a050e4783ee99c63c031a2e7f9a2eb7340d55505e17f30f", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3b43de24460d87c0971887286e7a20d40462e48eb7235954681a20cee25ddeb6"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"plug_crypto": {:hex, :plug_crypto, "1.2.0", "1cb20793aa63a6c619dd18bb33d7a3aa94818e5fd39ad357051a67f26dfa2df6", [:mix], [], "hexpm", "a48b538ae8bf381ffac344520755f3007cc10bd8e90b240af98ea29b69683fc2"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"postgrex": {:hex, :postgrex, "0.15.7", "724410acd48abac529d0faa6c2a379fb8ae2088e31247687b16cacc0e0883372", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "88310c010ff047cecd73d5ceca1d99205e4b1ab1b9abfdab7e00f5c9d20ef8f9"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.0", "4770888ef85599ead39c7f51d6b4b62306e602d96c69b2625d54dea3d9a5204b", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69e4e8e65b0ae077c9e14cd5f42c7cc486de0e07ac6e3409e6f0e52699a7872c"},
"tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"wallaby": {:hex, :wallaby, "0.26.0", "170b05b2fe572ec38071dbe45a908123959d5245f389f657e9a79eb463dc0431", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "07a437e75c9276900288e4fe5c1814a5486f10a54940aa524ea65ce22b40c182"},
"wallaby": {:hex, :wallaby, "0.28.0", "2ff217c0f245cadb3e5d91748ebcf0102873ceb9ef8a3507717c8bdd73915668", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.1.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "e58112650d0b51e81714a626eab7d486d7a77342c9bbc2ba262b6653f9b22558"},
"web_driver_client": {:hex, :web_driver_client, "0.1.0", "19466a989c76b7ec803c796cec0fec4611a64f445fd5120ce50c9e3817e09c2c", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c9c031ca915e8fc75b5e24ac93503244f3cc406dd7f53047087a45aa62d60e9e"},
}

View File

@ -2,33 +2,28 @@ defmodule TempleDemoWeb.TempleFeatureTest do
use ExUnit.Case, async: false
use Wallaby.Feature
alias TempleDemoWeb.Router.Helpers, as: Routes
alias TempleDemoWeb.Endpoint, as: E
@endpoint TempleDemoWeb.Endpoint
feature "renders the homepage", %{session: session} do
session
|> visit("/")
|> assert_text("Welcome to Phoenix!")
|> assert_text("inner content of outer")
end
feature "case statements work", %{session: session} do
session =
session
|> visit("/?text=staging")
session |> assert_text("Welcome to Phoenix!")
session |> assert_text("Peace-of-mind from prototype to staging")
session =
session
|> visit("/?text=foobar")
session |> assert_text("Welcome to Phoenix!")
session |> assert_text("Peace-of-mind from prototype to production")
session
|> visit("/?text=staging")
|> assert_text("Welcome to Phoenix!")
|> assert_text("Peace-of-mind from prototype to staging")
|> visit("/?text=foobar")
|> assert_text("Welcome to Phoenix!")
|> assert_text("Peace-of-mind from prototype to production")
end
feature "can create a new post", %{session: session} do
session
|> visit(Routes.post_path(E, :index))
|> visit(Routes.post_path(@endpoint, :index))
|> click(Query.link("New Post"))
|> fill_in(Query.text_field("Title"), with: "Temple is awesome!")
|> fill_in(Query.text_field("Body"), with: "In this post I will show you how to use Temple")

View File

@ -1,5 +1,8 @@
defmodule Temple.Buffer do
@moduledoc false
use Agent
def start_link(state \\ []) do
Agent.start_link(fn -> state end)
end

View File

@ -2,9 +2,7 @@ defmodule Temple do
alias Temple.Parser
@moduledoc """
> Warning: Docs are WIP
Temple syntax is available inside the `temple` and `live_temple` macros, and is compiled into EEx at build time.
Temple syntax is available inside the `temple`, and is compiled into EEx at build time.
### Usage
@ -47,7 +45,7 @@ defmodule Temple do
text_input f, :name
end
# You can explicitly call a tag by prefixing with the Temple module
# You can explicitly emit a tag by prefixing with the Temple module
Temple.div do
"Foo"
end
@ -59,11 +57,11 @@ defmodule Temple do
### Reserved keywords
You can pass a keyword list to an element as element attributes, but there are several reserved keywords.
You can pass a keyword list to an element as element attributes, but there is currently a reserved keyword.
#### Compact
Passing `compact: true` will not rendering new lines from within the element. This is useful if you are trying to use the `:empty` psuedo selector.
Passing `compact: true` will not emit a new line between the opening tag, the content, and the closing tag. This is useful if you are trying to use the `:empty` psuedo selector.
```elixir
temple do
@ -122,6 +120,27 @@ defmodule Temple do
end
end
@doc """
Context for temple markup.
Returns an EEx string.
## Usage
```elixir
import Temple
temple do
div class: @class do
"Hello, world!"
end
end
# <div class="<%= @class %>">
# Hello, world!
# </div>
```
"""
defmacro temple([do: block] = _block) do
markup = Parser.parse(block)
@ -136,9 +155,26 @@ defmodule Temple do
end
end
defmacro live_temple([do: block] = _block) do
@doc """
Compiles temple markup into a quoted expression using the given EEx Engine.
## Usage
```elixir
require Temple
Temple.compile Phoenix.HTML.Engine do
div class: @class do
"Hello, world!"
end
end
# Returns the same output that Phoenix templates output into the `render/1` function of their view modules.
```
"""
defmacro compile(engine, [do: block] = _block) do
markup = Parser.parse(block)
EEx.compile_string(markup, engine: Phoenix.LiveView.Engine)
EEx.compile_string(markup, engine: engine, line: __CALLER__.line, file: __CALLER__.file)
end
end

View File

@ -1,23 +1,161 @@
defmodule Temple.Component do
@moduledoc """
Behaviour for defining temple components.
"""
API for defining components.
@doc """
The render callback must return AST to be inserted into the markup.
Component modules are basically normal Phoenix View modules. The contents of the `render` macro are compiled into a `render/2` function. This means that you can define functions in your component module and use them in your component markup.
Since you need to return AST, it is typical to wrap the contents of the function in a `quote` block like:
Since component modules are view modules, the assigns you pass to the component are accessible via the `@` macro and the `assigns` variable.
## Usage
```elixir
defmodule MyAppWeb.Components.Flash do
use Temple.Component
def border_class(:info), do: "border-blue-500"
def border_class(:warning), do: "border-yellow-500"
def border_class(:error), do: "border-red-500"
def border_class(:success), do: "border-green-500"
render do
div class: "border rounded p-2 #\{assigns[:class]} #\{border_class(@message_type)}" do
@inner_content
end
end
end
```
Components are used by calling the `c` keyword, followed by the component module and any assigns you need to pass to the template.
`c` is a _**compile time keyword**_, not a function or a macro, so you won't see it in the generated documention.
```
@impl Temple.Component
def render() do
quote do
div do
@children
c MyAppWeb.Components.Flash, class: "font-bold", message_type: :info do
ul do
for info <- infos do
li class: "p-4" do
info.message
end
end
end
end
```
Since components are just modules, if you alias your module, you can use them more ergonomically.
```
alias MyAppWeb.Components.Flex
c Flex, class: "justify-between items center" do
for item <- items do
div class: "p-4" do
item.name
end
end
end
```
"""
@callback render() :: Macro.t()
defmacro __using__(_) do
quote do
import Temple.Component, only: [render: 1]
end
end
@doc """
Defines a component template.
## Usage
```elixir
defmodule MyAppWeb.Components.Flash do
use Temple.Component
def border_class(:info), do: "border-blue-500"
def border_class(:warning), do: "border-yellow-500"
def border_class(:error), do: "border-red-500"
def border_class(:success), do: "border-green-500"
render do
div class: "border rounded p-2 #\{assigns[:class]} #\{border_class(@message_type)}" do
@inner_content
end
end
end
```
"""
defmacro render(block) do
quote do
def render(assigns), do: render(:self, assigns)
def render(:self, var!(assigns)) do
require Temple
_ = var!(assigns)
Temple.compile(unquote(Temple.Component.engine()), unquote(block))
end
end
end
@doc """
Defines a component module.
This macro makes it easy to define components without creating a separate file. It literally inlines a component module.
Since it defines a module inside of the current module, local function calls from the outer module won't be available. For convenience, the outer module is aliased for you, so you can call remote functions with a shorter module name.
## Usage
```elixir
def MyAppWeb.SomeView do
use MyAppWeb.SomeView, :view
import Temple.Component, only: [defcomp: 2]
# define a function in outer module
def foobar(), do: "foobar"
# define a component
defcomp Button do
button id: SomeView.foobar(), # `MyAppWeb.SomeView` is aliased for you.
class: "text-sm px-3 py-2 rounded #\{assigns[:extra_classes]}",
type: "submit" do
@inner_content
end
end
end
# use the component in a SomeView template. Or else, you must alias `MyAppWeb.SomeView.Button`
c Button, extra_classes: "border-2 border-red-500" do
"Submit!"
end
```
"""
defmacro defcomp(module, [do: block] = _block) do
quote location: :keep do
defmodule unquote(module) do
use Temple.Component
alias unquote(__CALLER__.module)
render do
unquote(block)
end
end
end
end
@doc false
def engine() do
cond do
Code.ensure_loaded?(Phoenix.LiveView.Engine) ->
Phoenix.LiveView.Engine
Code.ensure_loaded?(Phoenix.HTML.Engine) ->
Phoenix.HTML.Engine
true ->
nil
end
end
end

View File

@ -1,19 +1,44 @@
defmodule Temple.Engine do
@behaviour Phoenix.Template.Engine
@moduledoc false
@moduledoc """
The Temple HTML engine makes it possible to use Temple with Phoenix controllers.
To get started, you will configure Phoenix to use this module for `.exs` files.
```elixir
# config.exs
config :phoenix, :template_engines,
# this will work for files named like `index.html.exs`
exs: Temple.Engine
# config/dev.exs
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$",
~r"lib/myapp_web/templates/.*(eex|exs|lexs)$"
]
]
# my_app/
# lib/
# my_app/
# my_app_web/
# templates/
# posts/
# show.html.exs
```
Now you can get started by writing `exs` files in the templates directory and they will be compiled as you would expect.
"""
def compile(path, _name) do
require Temple
template = path |> File.read!() |> Code.string_to_quoted!(file: path)
ast =
quote do
unquote(template)
end
Temple.temple(ast)
Temple.temple(template)
|> EEx.compile_string(engine: Phoenix.HTML.Engine, file: path, line: 1)
end
end

View File

@ -1,14 +1,46 @@
defmodule Temple.LiveViewEngine do
@behaviour Phoenix.Template.Engine
@moduledoc false
@moduledoc """
The Temple LiveView engine makes it possible to use Temple with Phoenix LiveView.
To get started, you will configure Phoenix to use this module for `.lexs` files.
```elixir
# config.exs
config :phoenix, :template_engines,
# this will work for files named like `index.html.lexs`
# you can enable Elixir syntax highlighting in your editor for this extension
lexs: Temple.LiveViewEngine
# config/dev.exs
config :your_app, YourAppWeb.Endpoint,
live_reload: [
patterns: [
~r"lib/myapp_web/(live|views)/.*(ex|exs|lexs)$",
~r"lib/myapp_web/templates/.*(eex|exs|lexs)$"
]
]
# my_app/
# lib/
# my_app/
# my_app_web/
# live/
# posts_live/
# show.ex
# show.html.lexs
```
Now you can get started by writing `lexs` files co-located with your live views and they will be compiled as you would expect.
"""
def compile(path, _name) do
require Temple
ast = path |> File.read!() |> Code.string_to_quoted!(file: path)
template = path |> File.read!() |> Code.string_to_quoted!(file: path)
Temple.temple(ast)
Temple.temple(template)
|> EEx.compile_string(engine: Phoenix.LiveView.Engine, file: path, line: 1)
end
end

View File

@ -1,4 +1,6 @@
defmodule Temple.Parser do
@moduledoc false
@doc """
Should return true if the parser should apply for the given AST.
"""

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.AnonymousFunctions do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,21 +1,18 @@
defmodule Temple.Parser.Components do
@moduledoc false
@behaviour Temple.Parser
@component_prefix Application.fetch_env!(:temple, :component_prefix)
alias Temple.Parser
alias Temple.Buffer
def applicable?({name, meta, _}) when is_atom(name) do
!meta[:temple_component_applied] &&
match?({:module, _}, name |> component_module() |> Code.ensure_compiled())
def applicable?({:c, _, _}) do
true
end
def applicable?(_), do: false
defp component_module(name) do
Module.concat([@component_prefix, Macro.camelize(to_string(name))])
end
def run({:c, _meta, [component_module | args]}, buffer) do
import Temple.Parser.Private
def run({name, _meta, args}, _buffer) do
{assigns, children} =
case args do
[assigns, [do: block]] ->
@ -31,45 +28,26 @@ defmodule Temple.Parser.Components do
{[], nil}
end
component_module = Module.concat([@component_prefix, Macro.camelize(to_string(name))])
if children do
Buffer.put(
buffer,
"<%= Phoenix.View.render_layout #{Macro.to_string(component_module)}, :self, #{
Macro.to_string(assigns)
} do %>"
)
ast = apply(component_module, :render, [])
traverse(buffer, children)
{name, meta, args} =
ast
|> Macro.prewalk(fn
{:@, _, [{:children, _, _}]} ->
children
Buffer.put(buffer, "<% end %>")
else
Buffer.put(
buffer,
"<%= Phoenix.View.render #{Macro.to_string(component_module)}, :self, #{
Macro.to_string(assigns)
} %>"
)
end
{:@, _, [{:temple, _, _}]} ->
assigns
{:@, _, [{name, _, _}]} = node ->
if name in Keyword.keys(assigns) do
Keyword.get(assigns, name, nil)
else
node
end
node ->
node
end)
ast =
if Enum.any?(
[
Parser.nonvoid_elements(),
Parser.nonvoid_elements_aliases(),
Parser.void_elements(),
Parser.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}
:ok
end
end

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.Default do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.DoExpressions do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.Empty do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.Match do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.NonvoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.RightArrow do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.TempleNamespaceNonvoid do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.TempleNamespaceVoid do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.Text do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Buffer

View File

@ -1,4 +1,5 @@
defmodule Temple.Parser.VoidElementsAliases do
@moduledoc false
@behaviour Temple.Parser
alias Temple.Parser

View File

@ -1,15 +0,0 @@
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

View File

@ -1,8 +0,0 @@
defmodule Temple.Utils do
@moduledoc false
def puts(binary) do
IO.puts(binary)
binary
end
end

View File

@ -15,10 +15,7 @@ defmodule Temple.MixProject do
source_url: "https://github.com/mhanberg/temple",
docs: [
main: "Temple",
extras: ["README.md"],
deps: [
phoenix_html: "https://hexdocs.pm/phoenix_html/"
]
extras: ["README.md"]
]
]
end
@ -48,7 +45,8 @@ defmodule Temple.MixProject do
[
{:ex_doc, "~> 0.22.0", only: :dev, runtime: false},
{:phoenix, ">= 0.0.0", optional: true},
{:phoenix_html, ">= 0.0.0", only: :test}
{:phoenix_html, ">= 0.0.0", only: :test},
{:phoenix_live_view, ">= 0.0.0", only: :test}
]
end
end

View File

@ -8,6 +8,7 @@
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.13.3", "2186c55cc7c54ca45b97c6f28cfd267d1c61b5f205f3c83533704cd991bdfdec", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "c6309a7da2e779cb9cdf2fb603d75f38f49ef324bedc7a81825998bd1744ff8a"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},

124
test/component_test.exs Normal file
View File

@ -0,0 +1,124 @@
defmodule Temple.ComponentTest do
use ExUnit.Case, async: true
use Temple
use Temple.Support.Utils
# `Phoenix.View.render_layout/4` is a phoenix function used for rendering partials that contain inner_content.
# These are usually layouts, but components that contain children are basically the same thing
test "renders components using Phoenix.View.render_layout" do
result =
temple do
div class: "font-bold" do
"Hello, world"
end
c Temple.Components.Component do
aside class: "foobar" do
"I'm a component!"
end
end
end
assert result ==
~s{<div class="font-bold">Hello, world</div><%= Phoenix.View.render_layout Temple.Components.Component, :self, [] do %><aside class="foobar">I'm a component!</aside><% end %>}
assert evaluate_template(result) ==
~s{<div class="font-bold">Hello, world</div><div><aside class="foobar">I'm a component!</aside></div>}
end
test "function components can accept local assigns" do
result =
temple do
div class: "font-bold" do
"Hello, world"
end
c Temple.Components.Component2, class: "bg-red" do
"I'm a component!"
end
end
assert result ==
~s{<div class="font-bold">Hello, world</div><%= Phoenix.View.render_layout Temple.Components.Component2, :self, [class: "bg-red"] do %>I'm a component!<% end %>}
assert evaluate_template(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"
c Temple.Components.Component2, class: class do
"I'm a component!"
end
end
assert result ==
~s{<div class="font-bold">Hello, world</div><% class = "bg-red" %><%= Phoenix.View.render_layout Temple.Components.Component2, :self, [class: class] do %>I'm a component!<% end %>}
end
test "function components can use other components" do
result =
temple do
c Temple.Components.Outer do
"outer!"
end
c Temple.Components.Inner, outer_id: "set by root inner" do
"inner!"
end
end
assert result ==
~s{<%= Phoenix.View.render_layout Temple.Components.Outer, :self, [] do %>outer!\n<% end %><%= Phoenix.View.render_layout Temple.Components.Inner, :self, [outer_id: "set by root inner"] do %>inner!\n<% end %>}
assert evaluate_template(result) == ~s"""
<div id="inner" outer-id="from-outer">outer!</div>
<div id="inner" outer-id="set by root inner">inner!</div>
"""
end
test "normal functions with blocks should be treated like if expressions" do
result =
temple do
leenk to: "/route", class: "foo" do
div class: "hi"
end
end
assert result ==
~s{<%= leenk(to: "/route", class: "foo") do %><div class="hi"></div><% end %>}
end
test "components can use functions from their modules" do
result =
temple do
c Temple.Components.WithFuncs, foo: :bar do
"doo doo"
end
end
assert result ==
~s{<%= Phoenix.View.render_layout Temple.Components.WithFuncs, :self, [foo: :bar] do %>doo doo<% end %>}
assert evaluate_template(result) == ~s{<div class="barbarbar">doo doo</div>}
end
test "components can be void elements" do
result =
temple do
c Temple.Components.VoidComponent, foo: :bar
end
assert result ==
~s{<%= Phoenix.View.render Temple.Components.VoidComponent, :self, [foo: :bar] %>}
assert evaluate_template(result) == ~s{<div class="void!!">bar</div>}
end
end

View File

@ -0,0 +1,51 @@
defmodule Temple.Parser.ComponentsTest do
use ExUnit.Case, async: false
alias Temple.Parser.Components
use Temple.Support.Utils
describe "applicable?/1" do
test "runs when using the `c` ast with a block" do
ast =
quote do
c SomeModule, foo: :bar do
div do
"hello"
end
end
end
assert Components.applicable?(ast)
end
test "runs when using the `c` ast without a block" do
ast =
quote do
c(SomeModule, foo: :bar)
end
assert Components.applicable?(ast)
end
end
describe "run/2" do
test "is correct" do
buf = start_supervised!(Temple.Buffer)
ast =
quote do
c SomeModule, foo: :bar do
aside class: "foobar" do
"I'm a component!"
end
end
end
Temple.Parser.Components.run(ast, buf)
result = Temple.Buffer.get(buf)
assert result ==
~s{<%= Phoenix.View.render_layout SomeModule, :self, [foo: :bar] do %><aside class="foobar">I'm a component!</aside><% end %>}
end
end
end

View File

@ -1,16 +0,0 @@
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

@ -1,12 +1,9 @@
defmodule Temple.Components.Component do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
div class: @assign do
@children
end
render do
div do
@inner_content
end
end
end

View File

@ -1,12 +1,9 @@
defmodule Temple.Components.Component2 do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
div class: @class do
@children
end
render do
div class: @class do
@inner_content
end
end
end

View File

@ -1,12 +1,9 @@
defmodule Temple.Components.HasTemple do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
div class: @temple[:class] do
@children
end
render do
div class: @temple[:class] do
@inner_content
end
end
end

View File

@ -1,12 +0,0 @@
defmodule Temple.Components.HasTempleFunctionAssign do
@behaviour Temple.Component
@impl Temple.Component
def render do
quote do
div Keyword.put(@temple, :class, "flex #{@temple[:class]}") do
@children
end
end
end
end

View File

@ -1,12 +1,9 @@
defmodule Temple.Components.Inner do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
div id: "inner", outer_id: @outer_id do
@children
end
render do
div id: "inner", outer_id: @outer_id do
@inner_content
end
end
end

View File

@ -1,12 +1,9 @@
defmodule Temple.Components.Outer do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
inner outer_id: "from-outer" do
@children
end
render do
c Temple.Components.Inner, outer_id: "from-outer" do
@inner_content
end
end
end

View File

@ -1,12 +1,9 @@
defmodule Temple.Components.Section do
@behaviour Temple.Component
use Temple.Component
@impl Temple.Component
def render do
quote do
section class: "foo!" do
@children
end
render do
section class: "foo!" do
@inner_content
end
end
end

View File

@ -0,0 +1,9 @@
defmodule Temple.Components.VoidComponent do
use Temple.Component
render do
div class: "void!!" do
"bar"
end
end
end

View File

@ -0,0 +1,17 @@
defmodule Temple.Components.WithFuncs do
use Temple.Component
def get_class(:bar) do
"barbarbar"
end
def get_class(_) do
"foofoofoo"
end
render do
div class: get_class(@foo) do
@inner_content
end
end
end

View File

@ -19,4 +19,12 @@ defmodule Temple.Support.Utils do
Kernel.=~(a, b)
end
def evaluate_template(template) do
template
|> EEx.compile_string(engine: Phoenix.HTML.Engine)
|> Code.eval_quoted([])
|> elem(0)
|> Phoenix.HTML.safe_to_string()
end
end

View File

@ -269,7 +269,7 @@ defmodule TempleTest do
~s{<div class="font-bold">Hello, world</div><div class="font-bold">Hello, world</div><div>Hello, world</div>}
end
test "passing 'compact: true' will not insert new lines" do
test "`do` passed as keyword will compile compacted markup" do
import Temple.Support.Utils, only: []
import Kernel
@ -287,95 +287,6 @@ 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
test "normal functions with blocks should be treated like if expressions" do
result =
temple do
leenk to: "/route", class: "foo" do
div class: "hi"
end
end
assert result ==
~s{<%= leenk(to: "/route", class: "foo") do %><div class="hi"></div><% end %>}
end
test "for with 2 generators" do
result =
temple do
@ -424,33 +335,4 @@ defmodule TempleTest do
assert result ==
~s{<fieldset<%= Temple.Parser.Private.runtime_attrs(Foo.foo_bar()) %>><input type="text"></fieldset>}
end
test "can pass a function as assigns that has @temple" do
result =
temple do
has_temple_function_assign class: "justify-end", style: "color: pink" do
input type: "text"
end
end
expected =
~S"""
<div<%= Temple.Parser.Private.runtime_attrs(Keyword.put([class: "justify-end", style: "color: pink"], :class, "flex #{[class: "justify-end", style: "color: pink"][:class]}")) %>>
<input type="text">
</div>
"""
|> String.trim()
assert result == expected
assert evaluate_template(result) == evaluate_template(expected)
end
defp evaluate_template(template) do
template
|> EEx.compile_string(engine: Phoenix.HTML.Engine)
|> Code.eval_quoted()
|> elem(0)
|> Phoenix.HTML.safe_to_string()
end
end