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,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 @@ +use Mix.Config 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/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, "") - 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, "") - 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, "") + 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, "") + 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 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{
Howdy!
} + 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{

Bob

\n

<%= foo %>

} 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{
Hello, world
I'm a component!
} + 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{
Hello, world
I'm a component!
} + 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{
Hello, world
<% class = "bg-red" %>
I'm a component!
} + end + + test "function components can use other components" do + result = + temple do + outer do + "outer!" + end + + inner do + "inner!" + end + end + + assert result == + ~s{
outer!
inner!
} + end + + test "@temple should be available in any component" do + result = + temple do + has_temple class: "boom" do + "yay!" + end + end + + assert result == ~s{
">yay!
} + end end