From e50743165b3e4c992efb5e5f430f226164435ddb Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Wed, 4 May 2022 23:09:47 -0400 Subject: [PATCH 1/9] Initial work to support dynamic components --- README.md | 4 +- lib/engines/mjml.ex | 46 +++++++++++++++---- lib/mjml_eex.ex | 30 ++++++------ lib/mjml_eex/component.ex | 4 +- lib/utils.ex | 10 +++- test/mjml_eex_test.exs | 18 ++++---- test/test_components/head_block.ex | 2 +- .../component_template.mjml.eex | 2 +- .../invalid_component_template.mjml.eex | 2 +- 9 files changed, 77 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index af2243b..e3db770 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ In order to render the email you would then call: `FunctionTemplate.render(first ### Using Components In addition to compiling single MJML EEx templates, you can also create MJML partials and include them -in other MJML templates AND components using the special `render_component` function. With the following +in other MJML templates AND components using the special `render_static_component` function. With the following modules: ```elixir @@ -170,7 +170,7 @@ And the following template: ```html - <%= render_component HeadBlock %> + <%= render_static_component HeadBlock %> diff --git a/lib/engines/mjml.ex b/lib/engines/mjml.ex index 8abe592..3127f69 100644 --- a/lib/engines/mjml.ex +++ b/lib/engines/mjml.ex @@ -10,10 +10,12 @@ defmodule MjmlEEx.Engines.Mjml do @impl true def init(opts) do {caller, remaining_opts} = Keyword.pop!(opts, :caller) + {mode, remaining_opts} = Keyword.pop!(remaining_opts, :mode) remaining_opts |> EEx.Engine.init() |> Map.put(:caller, caller) + |> Map.put(:mode, mode) end @impl true @@ -29,35 +31,63 @@ defmodule MjmlEEx.Engines.Mjml do defdelegate handle_text(state, meta, text), to: EEx.Engine @impl true - def handle_expr(state, "=", {:render_component, _, [{:__aliases__, _, _module} = aliases]}) do + def handle_expr(%{mode: :compile}, _marker, {:render_dynamic_component, _, _}) do + raise "render_dynamic_component can only be used with runtime compiled templates. Switch your template to `mode: :runtime`" + end + + def handle_expr(state, "=", {:render_dynamic_component, _, [{:__aliases__, _, _module} = aliases]}) do module = Macro.expand(aliases, state.caller) - do_render_component(state, module, [], state.caller) + do_render_dynamic_component(state, module, []) end - def handle_expr(state, "=", {:render_component, _, [{:__aliases__, _, _module} = aliases, opts]}) do + def handle_expr(state, "=", {:render_dynamic_component, _, [{:__aliases__, _, _module} = aliases, opts]}) do module = Macro.expand(aliases, state.caller) - do_render_component(state, module, opts, state.caller) + do_render_dynamic_component(state, module, opts) end - def handle_expr(_state, _marker, {:render_component, _, _}) do - raise "render_component can only be invoked inside of an <%= ... %> expression" + def handle_expr(_state, _marker, {:render_dynamic_component, _, _}) do + raise "render_dynamic_component can only be invoked inside of an <%= ... %> expression" + end + + def handle_expr(state, "=", {:render_static_component, _, [{:__aliases__, _, _module} = aliases]}) do + module = Macro.expand(aliases, state.caller) + + do_render_static_component(state, module, []) + end + + def handle_expr(state, "=", {:render_static_component, _, [{:__aliases__, _, _module} = aliases, opts]}) do + module = Macro.expand(aliases, state.caller) + + do_render_static_component(state, module, opts) + end + + def handle_expr(_state, _marker, {:render_static_component, _, _}) do + raise "render_static_component can only be invoked inside of an <%= ... %> expression" end def handle_expr(_state, marker, expr) do raise "Unescaped expression. This should never happen and is most likely a bug in MJML EEx: <%#{marker} #{Macro.to_string(expr)} %>" end - defp do_render_component(state, module, opts, caller) do + defp do_render_static_component(state, module, opts) do {mjml_component, _} = module |> apply(:render, [opts]) |> Utils.escape_eex_expressions() - |> EEx.compile_string(engine: MjmlEEx.Engines.Mjml, line: 1, trim: true, caller: caller) + |> EEx.compile_string(engine: MjmlEEx.Engines.Mjml, line: 1, trim: true, caller: state.caller, mode: state.mode) |> Code.eval_quoted() %{binary: binary} = state %{state | binary: [mjml_component | binary]} end + + defp do_render_dynamic_component(state, module, opts) do + mjml_component = + "<%= Phoenix.HTML.raw(MjmlEEx.Utils.render_dynamic_component(#{module}, #{Macro.to_string(opts)})) %>" + + %{binary: binary} = state + %{state | binary: [mjml_component | binary]} + end end diff --git a/lib/mjml_eex.ex b/lib/mjml_eex.ex index af02e25..7cd0f59 100644 --- a/lib/mjml_eex.ex +++ b/lib/mjml_eex.ex @@ -37,18 +37,14 @@ defmodule MjmlEEx do alias MjmlEEx.Utils defmacro __using__(opts) do - mjml_template = - case Keyword.fetch(opts, :mjml_template) do - {:ok, mjml_template} -> - %Macro.Env{file: calling_module_file} = __CALLER__ + # Get some data about the calling module + %Macro.Env{file: calling_module_file} = __CALLER__ + module_directory = Path.dirname(calling_module_file) + file_minus_extension = Path.basename(calling_module_file, ".ex") + mjml_template_file = Keyword.get(opts, :mjml_template, "#{file_minus_extension}.mjml.eex") - calling_module_file - |> Path.dirname() - |> Path.join(mjml_template) - - :error -> - raise "The :mjml_template option is required." - end + # The absolute path of the mjml template + mjml_template = Path.join(module_directory, mjml_template_file) unless File.exists?(mjml_template) do raise "The provided :mjml_template does not exist at #{inspect(mjml_template)}." @@ -65,10 +61,10 @@ defmodule MjmlEEx do raw_mjml_template = case layout_module do :none -> - get_raw_template(mjml_template, __CALLER__) + get_raw_template(mjml_template, compilation_mode, __CALLER__) module when is_atom(module) -> - get_raw_template_with_layout(mjml_template, layout_module, __CALLER__) + get_raw_template_with_layout(mjml_template, layout_module, compilation_mode, __CALLER__) invalid_layout -> raise "#{inspect(invalid_layout)} is an invalid layout option" @@ -159,18 +155,18 @@ defmodule MjmlEEx do raise "#{inspect(invalid_mode)} is an invalid :mode. Possible values are :runtime or :compile" end - defp get_raw_template(template_path, caller) do + defp get_raw_template(template_path, mode, caller) do {mjml_document, _} = template_path |> File.read!() |> Utils.escape_eex_expressions() - |> EEx.compile_string(engine: MjmlEEx.Engines.Mjml, line: 1, trim: true, caller: caller) + |> EEx.compile_string(engine: MjmlEEx.Engines.Mjml, line: 1, trim: true, caller: caller, mode: mode) |> Code.eval_quoted() Utils.decode_eex_expressions(mjml_document) end - defp get_raw_template_with_layout(template_path, layout_module, caller) do + defp get_raw_template_with_layout(template_path, layout_module, mode, caller) do template_file_contents = File.read!(template_path) pre_inner_content = layout_module.pre_inner_content() post_inner_content = layout_module.post_inner_content() @@ -179,7 +175,7 @@ defmodule MjmlEEx do [pre_inner_content, template_file_contents, post_inner_content] |> Enum.join() |> Utils.escape_eex_expressions() - |> EEx.compile_string(engine: MjmlEEx.Engines.Mjml, line: 1, trim: true, caller: caller) + |> EEx.compile_string(engine: MjmlEEx.Engines.Mjml, line: 1, trim: true, caller: caller, mode: mode) |> Code.eval_quoted() Utils.decode_eex_expressions(mjml_document) diff --git a/lib/mjml_eex/component.ex b/lib/mjml_eex/component.ex index 2f5aaee..8b4be32 100644 --- a/lib/mjml_eex/component.ex +++ b/lib/mjml_eex/component.ex @@ -22,7 +22,7 @@ defmodule MjmlEEx.Component do ``` With that in place, anywhere that you would like to use the component, you can add: - `<%= render_component HeadBlock %>` in your MJML EEx template. + `<%= render_static_component HeadBlock %>` in your MJML EEx template. You can also pass options to the render function like so: @@ -42,7 +42,7 @@ defmodule MjmlEEx.Component do end ``` - And calling it like so: `<%= render_component(HeadBlock, title: "Some really cool title") %>` + And calling it like so: `<%= render_static_component(HeadBlock, title: "Some really cool title") %>` """ @doc """ diff --git a/lib/utils.ex b/lib/utils.ex index ef82231..9df7a08 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -4,6 +4,8 @@ defmodule MjmlEEx.Utils do Elixir expressions in MJML EEx templates. """ + @mjml_eex_special_expressions [:render_static_component, :render_dynamic_component] + @doc """ This function encodes the internals of an MJML EEx document so that when it is compiled, the EEx expressions don't break @@ -51,6 +53,12 @@ defmodule MjmlEEx.Utils do end end + @doc false + def render_dynamic_component(module, opts) do + module + |> apply(:render, [opts]) + end + defp reduce_tokens(tokens) do tokens |> Enum.reduce("", fn @@ -65,7 +73,7 @@ defmodule MjmlEEx.Utils do |> Code.string_to_quoted() case captured_expression do - {:ok, {:render_component, _line, _args}} -> + {:ok, {special_expression, _line, _args}} when special_expression in @mjml_eex_special_expressions -> acc <> "<%#{normalize_marker(marker)} #{List.to_string(expression)} %>" _ -> diff --git a/test/mjml_eex_test.exs b/test/mjml_eex_test.exs index f06a8ef..dc3e28e 100644 --- a/test/mjml_eex_test.exs +++ b/test/mjml_eex_test.exs @@ -124,14 +124,16 @@ defmodule MjmlEExTest do end describe "InvalidComponentTemplate" do - test "should fail to compile since the render_component call is not in an = expression" do - assert_raise RuntimeError, ~r/render_component can only be invoked inside of an <%= ... %> expression/, fn -> - defmodule InvalidTemplateOption do - use MjmlEEx, - mjml_template: "test_templates/invalid_component_template.mjml.eex", - mode: :compile - end - end + test "should fail to compile since the render_static_component call is not in an = expression" do + assert_raise RuntimeError, + ~r/render_static_component can only be invoked inside of an <%= ... %> expression/, + fn -> + defmodule InvalidTemplateOption do + use MjmlEEx, + mjml_template: "test_templates/invalid_component_template.mjml.eex", + mode: :compile + end + end end end diff --git a/test/test_components/head_block.ex b/test/test_components/head_block.ex index 8ff006c..dd48600 100644 --- a/test/test_components/head_block.ex +++ b/test/test_components/head_block.ex @@ -15,7 +15,7 @@ defmodule MjmlEEx.TestComponents.HeadBlock do #{opts[:title]} - <%= render_component MjmlEEx.TestComponents.AttributeBlock %> + <%= render_static_component MjmlEEx.TestComponents.AttributeBlock %> """ end diff --git a/test/test_templates/component_template.mjml.eex b/test/test_templates/component_template.mjml.eex index 2b7c200..ab06dbc 100644 --- a/test/test_templates/component_template.mjml.eex +++ b/test/test_templates/component_template.mjml.eex @@ -1,5 +1,5 @@ - <%= render_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> + <%= render_static_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> diff --git a/test/test_templates/invalid_component_template.mjml.eex b/test/test_templates/invalid_component_template.mjml.eex index 6d4046f..5752f7c 100644 --- a/test/test_templates/invalid_component_template.mjml.eex +++ b/test/test_templates/invalid_component_template.mjml.eex @@ -1,5 +1,5 @@ - <% render_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> + <% render_static_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> From 351e09b90ffe4e9ea5cc5082eab290b3c236bce8 Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Wed, 4 May 2022 23:10:02 -0400 Subject: [PATCH 2/9] Updating test --- test/mjml_eex_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mjml_eex_test.exs b/test/mjml_eex_test.exs index dc3e28e..bccfe44 100644 --- a/test/mjml_eex_test.exs +++ b/test/mjml_eex_test.exs @@ -97,8 +97,8 @@ defmodule MjmlEExTest do end describe "The use macro" do - test "should fail to compile since a required option is not present" do - assert_raise RuntimeError, ~r/The :mjml_template option is required./, fn -> + test "should fail to compile since a valid mjml template can not be found" do + assert_raise RuntimeError, ~r/The provided :mjml_template does not exist at/, fn -> defmodule NoTemplateOption do use MjmlEEx end From d71f41f575766bb840fdba71d0482198753ebcd7 Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Thu, 5 May 2022 17:37:03 -0400 Subject: [PATCH 3/9] More dynamic component work --- lib/engines/mjml.ex | 16 ++++++++++++++-- lib/utils.ex | 23 ++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/lib/engines/mjml.ex b/lib/engines/mjml.ex index 3127f69..3db3a57 100644 --- a/lib/engines/mjml.ex +++ b/lib/engines/mjml.ex @@ -11,11 +11,13 @@ defmodule MjmlEEx.Engines.Mjml do def init(opts) do {caller, remaining_opts} = Keyword.pop!(opts, :caller) {mode, remaining_opts} = Keyword.pop!(remaining_opts, :mode) + {rendering_dynamic_component, remaining_opts} = Keyword.pop(remaining_opts, :rendering_dynamic_component, false) remaining_opts |> EEx.Engine.init() |> Map.put(:caller, caller) |> Map.put(:mode, mode) + |> Map.put(:rendering_dynamic_component, rendering_dynamic_component) end @impl true @@ -35,6 +37,10 @@ defmodule MjmlEEx.Engines.Mjml do raise "render_dynamic_component can only be used with runtime compiled templates. Switch your template to `mode: :runtime`" end + def handle_expr(%{rendering_dynamic_component: true}, _marker, {:render_dynamic_component, _, _}) do + raise "Cannot call `render_dynamic_component` inside of another dynamically rendered component" + end + def handle_expr(state, "=", {:render_dynamic_component, _, [{:__aliases__, _, _module} = aliases]}) do module = Macro.expand(aliases, state.caller) @@ -68,7 +74,7 @@ defmodule MjmlEEx.Engines.Mjml do end def handle_expr(_state, marker, expr) do - raise "Unescaped expression. This should never happen and is most likely a bug in MJML EEx: <%#{marker} #{Macro.to_string(expr)} %>" + raise "Invalid expression. Components can only have `render_static_component` and `render_dynamic_component` EEx expression: <%#{marker} #{Macro.to_string(expr)} %>" end defp do_render_static_component(state, module, opts) do @@ -84,8 +90,14 @@ defmodule MjmlEEx.Engines.Mjml do end defp do_render_dynamic_component(state, module, opts) do + caller = + state + |> Map.get(:caller) + |> :erlang.term_to_binary() + |> Base.encode64() + mjml_component = - "<%= Phoenix.HTML.raw(MjmlEEx.Utils.render_dynamic_component(#{module}, #{Macro.to_string(opts)})) %>" + "<%= Phoenix.HTML.raw(MjmlEEx.Utils.render_dynamic_component(#{module}, #{Macro.to_string(opts)}, \"#{caller}\")) %>" %{binary: binary} = state %{state | binary: [mjml_component | binary]} diff --git a/lib/utils.ex b/lib/utils.ex index 9df7a08..2dfd267 100644 --- a/lib/utils.ex +++ b/lib/utils.ex @@ -54,9 +54,26 @@ defmodule MjmlEEx.Utils do end @doc false - def render_dynamic_component(module, opts) do - module - |> apply(:render, [opts]) + def render_dynamic_component(module, opts, caller) do + caller = + caller + |> Base.decode64!() + |> :erlang.binary_to_term() + + {mjml_component, _} = + module + |> apply(:render, [opts]) + |> EEx.compile_string( + engine: MjmlEEx.Engines.Mjml, + line: 1, + trim: true, + caller: caller, + mode: :runtime, + rendering_dynamic_component: true + ) + |> Code.eval_quoted() + + mjml_component end defp reduce_tokens(tokens) do From 9a5bafd31a57c4102e881d06983003ac4bcfe937 Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Thu, 5 May 2022 17:59:58 -0400 Subject: [PATCH 4/9] Adding tests for dynamic components --- lib/mjml_eex.ex | 3 - test/mjml_eex_test.exs | 38 +++++++++++++ test/test_components/dynamic_component.ex | 16 ++++++ .../dynamic_component_template.mjml.eex | 55 +++++++++++++++++++ 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 test/test_components/dynamic_component.ex create mode 100644 test/test_templates/dynamic_component_template.mjml.eex diff --git a/lib/mjml_eex.ex b/lib/mjml_eex.ex index 7cd0f59..3619241 100644 --- a/lib/mjml_eex.ex +++ b/lib/mjml_eex.ex @@ -65,9 +65,6 @@ defmodule MjmlEEx do module when is_atom(module) -> get_raw_template_with_layout(mjml_template, layout_module, compilation_mode, __CALLER__) - - invalid_layout -> - raise "#{inspect(invalid_layout)} is an invalid layout option" end generate_functions(compilation_mode, raw_mjml_template, mjml_template, layout_module) diff --git a/test/mjml_eex_test.exs b/test/mjml_eex_test.exs index bccfe44..fc368ff 100644 --- a/test/mjml_eex_test.exs +++ b/test/mjml_eex_test.exs @@ -19,6 +19,12 @@ defmodule MjmlEExTest do mode: :compile end + defmodule DynamicComponentTemplate do + use MjmlEEx, + mjml_template: "test_templates/dynamic_component_template.mjml.eex", + mode: :runtime + end + defmodule FunctionTemplate do use MjmlEEx, mjml_template: "test_templates/function_template.mjml.eex", @@ -94,6 +100,26 @@ defmodule MjmlEExTest do end end end + + test "should raise an error if the MJML template compile mode is invalid" do + assert_raise RuntimeError, ~r/:yolo is an invalid :mode. Possible values are :runtime or :compile/, fn -> + defmodule InvalidCompileModeOption do + use MjmlEEx, + mjml_template: "test_templates/invalid_template.mjml.eex", + mode: :yolo + end + end + end + + test "should raise an error if the layout option is invalid" do + assert_raise ArgumentError, ~r/could not load module InvalidModule due to reason/, fn -> + defmodule InvalidLayoutOption do + use MjmlEEx, + mjml_template: "test_templates/invalid_template.mjml.eex", + layout: InvalidModule + end + end + end end describe "The use macro" do @@ -123,6 +149,18 @@ defmodule MjmlEExTest do end end + describe "DynamicComponentTemplate.render/1" do + test "should render the document with the appropriate assigns" do + rendered_template = DynamicComponentTemplate.render(some_data: 1..5) + + assert rendered_template =~ "Some data - 1" + assert rendered_template =~ "Some data - 2" + assert rendered_template =~ "Some data - 3" + assert rendered_template =~ "Some data - 4" + assert rendered_template =~ "Some data - 5" + end + end + describe "InvalidComponentTemplate" do test "should fail to compile since the render_static_component call is not in an = expression" do assert_raise RuntimeError, diff --git a/test/test_components/dynamic_component.ex b/test/test_components/dynamic_component.ex new file mode 100644 index 0000000..0744c09 --- /dev/null +++ b/test/test_components/dynamic_component.ex @@ -0,0 +1,16 @@ +defmodule MjmlEEx.TestComponents.DynamicComponent do + @moduledoc """ + This module defines the MJML component for the shared head block. + """ + + use MjmlEEx.Component + + @impl true + def render(data: data) do + """ +

+ #{data} +

+ """ + end +end diff --git a/test/test_templates/dynamic_component_template.mjml.eex b/test/test_templates/dynamic_component_template.mjml.eex new file mode 100644 index 0000000..281e743 --- /dev/null +++ b/test/test_templates/dynamic_component_template.mjml.eex @@ -0,0 +1,55 @@ + + <%= render_static_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> + + + + Writing A Good Headline For Your Advertisement + + + + + // BR&AND + + + HOME   /   SERVICE   /   THIRD + + + + + Free Advertising For Your Online Business. + + + + + + + + + + A Right Media Mix Can Make The Difference. + + + + + + Marketers/advertisers usually focus their efforts on the people responsible for making the purchase. In many cases, this is an effective approach but in other cases it can make for a totally useless marketing campaign. + <%= for index <- @some_data do %> + <%= render_dynamic_component MjmlEEx.TestComponents.DynamicComponent, data: "Some data - #{index}" %> + <% end %> + + SIGN UP TODAY!! + + + + + + + + + + + Unsubscribe from this newsletter
52 Edison Court Suite 259 / East Aidabury / Cambodi
Made by svenhaustein.de
+
+
+
+
From 5ec93f3af1165a5b13ab823b3266e38cd00ef10e Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Thu, 5 May 2022 23:21:23 -0400 Subject: [PATCH 5/9] Updating changelog and readme in preparation for new release --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 4 ++-- mix.exs | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b0127..f17073c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2021-05-06 + +### Added + +- The `render_static_component` function can be used to render components that don't make use of any assigns. For + example, in your template you would have: `<%= render_static_component MyCoolComponent, static: "data" %>` and this + can be rendered at compile time as well as runtime. +- The `render_dynamic_component` function can be used to render components that make use of assigns at runtime. For + example, in your template you would have: `<%= render_dynamic_component MyCoolComponent, static: @data %>`. + +### Changed + +- When calling `use MjmlEEx`, if the `:mjml_template` option is not provided, the module attempts to find a template + file that has the same file name as the module (with the `.mjml.eex` extension instead of `.ex`). + +### Removed + +- `render_component` is no longer available and users should now use `render_static_component` or + `render_dynamic_component`. + ## [0.5.0] - 2021-04-28 ### Added diff --git a/README.md b/README.md index e3db770..f2d2158 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ dependencies in `mix.exs`: ```elixir def deps do [ - {:mjml_eex, "~> 0.5.0"} + {:mjml_eex, "~> 0.6.0"} ] end ``` @@ -78,7 +78,7 @@ Checkout my [GitHub Sponsorship page](https://github.com/sponsors/akoutmos) if y ### Basic Usage -Add `{:mjml_eex, "~> 0.5.0"}` to your `mix.exs` file and run `mix deps.get`. After you have that in place, you +Add `{:mjml_eex, "~> 0.6.0"}` to your `mix.exs` file and run `mix deps.get`. After you have that in place, you can go ahead and create a template module like so: ```elixir diff --git a/mix.exs b/mix.exs index a43a574..1821c31 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule MjmlEEx.MixProject do def project do [ app: :mjml_eex, - version: "0.5.0", + version: "0.6.0", elixir: ">= 1.11.0", elixirc_paths: elixirc_paths(Mix.env()), name: "MJML EEx", From 7945a7302c2c01384bb9d3c571e464ec828e0dda Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Fri, 6 May 2022 10:07:55 -0400 Subject: [PATCH 6/9] More tests --- lib/engines/mjml.ex | 2 +- test/mjml_eex_test.exs | 30 ++++++++++ .../invalid_dynamic_component.ex | 16 ++++++ ...nvalid_dynamic_component_template.mjml.eex | 55 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 test/test_components/invalid_dynamic_component.ex create mode 100644 test/test_templates/invalid_dynamic_component_template.mjml.eex diff --git a/lib/engines/mjml.ex b/lib/engines/mjml.ex index 3db3a57..5f9f755 100644 --- a/lib/engines/mjml.ex +++ b/lib/engines/mjml.ex @@ -34,7 +34,7 @@ defmodule MjmlEEx.Engines.Mjml do @impl true def handle_expr(%{mode: :compile}, _marker, {:render_dynamic_component, _, _}) do - raise "render_dynamic_component can only be used with runtime compiled templates. Switch your template to `mode: :runtime`" + raise "render_dynamic_component can only be used with runtime generated templates. Switch your template to `mode: :runtime`" end def handle_expr(%{rendering_dynamic_component: true}, _marker, {:render_dynamic_component, _, _}) do diff --git a/test/mjml_eex_test.exs b/test/mjml_eex_test.exs index fc368ff..cf1177c 100644 --- a/test/mjml_eex_test.exs +++ b/test/mjml_eex_test.exs @@ -25,6 +25,12 @@ defmodule MjmlEExTest do mode: :runtime end + defmodule InvalidDynamicComponentTemplate do + use MjmlEEx, + mjml_template: "test_templates/invalid_dynamic_component_template.mjml.eex", + mode: :runtime + end + defmodule FunctionTemplate do use MjmlEEx, mjml_template: "test_templates/function_template.mjml.eex", @@ -161,6 +167,30 @@ defmodule MjmlEExTest do end end + describe "CompileTimeDynamicComponentTemplate.render/1" do + test "should raise an error if a dynamic component is rendered at compile time" do + assert_raise RuntimeError, + ~r/render_dynamic_component can only be used with runtime generated templates. Switch your template to `mode: :runtime`/, + fn -> + defmodule CompileTimeDynamicComponentTemplate do + use MjmlEEx, + mjml_template: "test_templates/dynamic_component_template.mjml.eex", + mode: :compile + end + end + end + end + + describe "InvalidDynamicComponentTemplate.render/1" do + test "should raise an error as dynamic components cannot render other dynamic components" do + assert_raise RuntimeError, + ~r/Cannot call `render_dynamic_component` inside of another dynamically rendered component/, + fn -> + InvalidDynamicComponentTemplate.render(some_data: 1..5) + end + end + end + describe "InvalidComponentTemplate" do test "should fail to compile since the render_static_component call is not in an = expression" do assert_raise RuntimeError, diff --git a/test/test_components/invalid_dynamic_component.ex b/test/test_components/invalid_dynamic_component.ex new file mode 100644 index 0000000..ef39b06 --- /dev/null +++ b/test/test_components/invalid_dynamic_component.ex @@ -0,0 +1,16 @@ +defmodule MjmlEEx.TestComponents.InvalidDynamicComponent do + @moduledoc """ + This module defines the MJML component for the shared head block. + """ + + use MjmlEEx.Component + + @impl true + def render(data: data) do + """ +

+ <%= render_dynamic_component MjmlEEx.TestComponents.DynamicComponent %> +

+ """ + end +end diff --git a/test/test_templates/invalid_dynamic_component_template.mjml.eex b/test/test_templates/invalid_dynamic_component_template.mjml.eex new file mode 100644 index 0000000..52e4710 --- /dev/null +++ b/test/test_templates/invalid_dynamic_component_template.mjml.eex @@ -0,0 +1,55 @@ + + <%= render_static_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> + + + + Writing A Good Headline For Your Advertisement + + + + + // BR&AND + + + HOME   /   SERVICE   /   THIRD + + + + + Free Advertising For Your Online Business. + + + + + + + + + + A Right Media Mix Can Make The Difference. + + + + + + Marketers/advertisers usually focus their efforts on the people responsible for making the purchase. In many cases, this is an effective approach but in other cases it can make for a totally useless marketing campaign. + <%= for index <- @some_data do %> + <%= render_dynamic_component MjmlEEx.TestComponents.InvalidDynamicComponent, data: "Some data - #{index}" %> + <% end %> + + SIGN UP TODAY!! + + + + + + + + + + + Unsubscribe from this newsletter
52 Edison Court Suite 259 / East Aidabury / Cambodi
Made by svenhaustein.de
+
+
+
+
From 8fd13838e7e52bbab2e05fd5855a645bf6ef353c Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Fri, 6 May 2022 10:25:45 -0400 Subject: [PATCH 7/9] More tests --- test/mjml_eex_test.exs | 14 +++++ ...ession_dynamic_component_template.mjml.eex | 52 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 test/test_templates/bad_expression_dynamic_component_template.mjml.eex diff --git a/test/mjml_eex_test.exs b/test/mjml_eex_test.exs index cf1177c..288f969 100644 --- a/test/mjml_eex_test.exs +++ b/test/mjml_eex_test.exs @@ -191,6 +191,20 @@ defmodule MjmlEExTest do end end + describe "BadExpressionDynamicComponentTemplate" do + test "should fail to compile since the render_dynamic_component call is not in an = expression" do + assert_raise RuntimeError, + ~r/render_dynamic_component can only be invoked inside of an <%= ... %> expression/, + fn -> + defmodule BadExpressionDynamicComponentTemplate do + use MjmlEEx, + mjml_template: "test_templates/bad_expression_dynamic_component_template.mjml.eex", + mode: :runtime + end + end + end + end + describe "InvalidComponentTemplate" do test "should fail to compile since the render_static_component call is not in an = expression" do assert_raise RuntimeError, diff --git a/test/test_templates/bad_expression_dynamic_component_template.mjml.eex b/test/test_templates/bad_expression_dynamic_component_template.mjml.eex new file mode 100644 index 0000000..2d04854 --- /dev/null +++ b/test/test_templates/bad_expression_dynamic_component_template.mjml.eex @@ -0,0 +1,52 @@ + + <% render_dynamic_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> + + + + Writing A Good Headline For Your Advertisement + + + + + // BR&AND + + + HOME   /   SERVICE   /   THIRD + + + + + Free Advertising For Your Online Business. + + + + + + + + + + A Right Media Mix Can Make The Difference. + + + + + + Marketers/advertisers usually focus their efforts on the people responsible for making the purchase. In many cases, this is an effective approach but in other cases it can make for a totally useless marketing campaign. + + <%= @call_to_action_text %> + + + + + + + + + + + Unsubscribe from this newsletter
52 Edison Court Suite 259 / East Aidabury / Cambodi
Made by svenhaustein.de
+
+
+
+
From fa907b237d3aeac0fbf06ad0e211f5b7ca776e4e Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Fri, 6 May 2022 10:28:04 -0400 Subject: [PATCH 8/9] More tests --- test/test_templates/dynamic_component_template.mjml.eex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_templates/dynamic_component_template.mjml.eex b/test/test_templates/dynamic_component_template.mjml.eex index 281e743..bc3f6aa 100644 --- a/test/test_templates/dynamic_component_template.mjml.eex +++ b/test/test_templates/dynamic_component_template.mjml.eex @@ -1,5 +1,5 @@ - <%= render_static_component(MjmlEEx.TestComponents.HeadBlock, [title: "Hello!"]) %> + <%= render_dynamic_component MjmlEEx.TestComponents.HeadBlock %> From 4b0ed4a8a00ead9a3f9dda46ee8b84649f900739 Mon Sep 17 00:00:00 2001 From: Alex Koutmos Date: Fri, 6 May 2022 18:56:01 -0400 Subject: [PATCH 9/9] Updating docs --- CHANGELOG.md | 3 ++- README.md | 21 +++++++++++++++++++-- lib/mjml_eex.ex | 26 +++++++++++++++++++++++++- lib/mjml_eex/component.ex | 16 ++++++++++++---- 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17073c..1e4dbed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - When calling `use MjmlEEx`, if the `:mjml_template` option is not provided, the module attempts to find a template - file that has the same file name as the module (with the `.mjml.eex` extension instead of `.ex`). + file in the same directory that has the same file name as the module (with the `.mjml.eex` extension instead + of `.ex`). This functions similar to how Phoenix and LiveView handle their templates. ### Removed diff --git a/README.md b/README.md index f2d2158..8171c62 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,25 @@ And the following template: ``` -Be sure to look at the `MjmlEEx.Component` for additional usage information as you can also pass options -to your template and use them when generating the partial string. +Be sure to look at the `MjmlEEx.Component` module for additional usage information as you can also pass options to your +template and use them when generating the partial string. One thing to note is that when using +`render_static_component`, the data that is passed to the component must be defined at compile time. This means that you +cannot use any assigns that would bee to be evaluated at runtime. For example, this would raise an error: + +```elixir + + <%= render_static_component MyTextComponent, some_data: @some_data %> + +``` + +If you need to render your components dynamically, use `render_dynamic_component` instead and be sure to configure your +template module like so to generate the email HTML at runtime: + +```elixir +def MyTemplate do + use MjmlEEx, mode: :runtime +end +``` ### Using Layouts diff --git a/lib/mjml_eex.ex b/lib/mjml_eex.ex index 3619241..d9022c7 100644 --- a/lib/mjml_eex.ex +++ b/lib/mjml_eex.ex @@ -1,7 +1,31 @@ defmodule MjmlEEx do @moduledoc """ Documentation for `MjmlEEx` template module. This moule contains the macro - that is used to create an MJML EEx template. + that is used to create an MJML EEx template. The macro can be configured to + render the MJML template in a few different ways, so be sure to read the + option documentation. + + ## Macro Options + + - `:mjml_template`- A binary that specifies the name of the `.mjml.eex` template that the module will compile. The + directory path is relative to the template module. If this option is not provided, the MjmlEEx will look for a + file that has the same name as the module but with the `.mjml.ex` extension as opposed to `.ex`. + + - `:mode`- This option defines when the MJML template is actually compiled. The possible values are `:runtime` and + `:compile`. When this option is set to `:compile`, the MJML template is compiled into email compatible HTML at + compile time. It is suggested that this mode is only used if the template is relatively simple and there are only + assigns being used as text or attributes on html elements (as opposed to attributes on MJML elements). The reason + for that being that these assigns may be discarded as part of the MJML compilation phase. On the plus side, you + do get a performance bump here since the HTML for the email is already generated. When this is set to `:runtime`, + the MJML template is compiled at runtime and all the template assigns are applied prior to the MJML compilation + phase. These means that there is a performance hit since you are compiling the MJML template every time, but the + template can use more complex EEx constructs like `for`, `case` and `cond`. The default configuration is `:runtime`. + + - `:layout` - This option defines what layout the template should be injected into prior to rendering the template. + This is useful if you want to have reusable email templates in order to keep your email code DRY and reusable. + Your template will then be injected into the layout where the layout defines `<%= inner_content %>`. + + ## Example Usage You can use this module like so: diff --git a/lib/mjml_eex/component.ex b/lib/mjml_eex/component.ex index 8b4be32..2d1d28f 100644 --- a/lib/mjml_eex/component.ex +++ b/lib/mjml_eex/component.ex @@ -1,9 +1,17 @@ defmodule MjmlEEx.Component do @moduledoc """ - This module allows you to define a reusable MJML component that - can be injected into an MJML template prior to it being - rendered into HTML. To do so, create an `MjmlEEx.Component` - module that looks like so: + This module allows you to define a reusable MJML component that can be injected into + an MJML template prior to it being rendered into HTML. There are two different ways + that components can be rendered in templates. The first being `render_static_component` + and the other being `render_dynamic_component`. `render_static_component` should be used + to render the component when the data provided to the component is known at compile time. + If you want to dynamically render a component (make sure that the template is set to + `mode: :runtime`) with assigns that are passed to the template, then use + `render_dynamic_component`. + + ## Example Usage + + To use an MjmlEEx component, create an `MjmlEEx.Component` module that looks like so: ```elixir defmodule HeadBlock do