Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phoenix 1.7 and phasing out Phoenix.View #287

Open
adamu opened this issue Dec 5, 2022 · 27 comments
Open

Phoenix 1.7 and phasing out Phoenix.View #287

adamu opened this issue Dec 5, 2022 · 27 comments

Comments

@adamu
Copy link

adamu commented Dec 5, 2022

For new projects generated with Phoenix 1.7 Phoenix.View has been replaced with Phoenix.Component. Are there any plans to update Phoenix.Swoosh to reflect this?

@princemaple
Copy link
Member

I haven't got around to test the changes yet, so no idea.

PRs are welcome, if people think there is necessary and/or beneficial change.

@iangreenleaf
Copy link

I am upgrading my small app to 1.7.0-rc.2 at the moment. I'm dropping View entirely, so I wanted to get emails using the new component rendering as well. It didn't look like that would be possible with Phoenix.Swoosh, so I dropped it and wrote some simple little helpers instead. I'm not sure I found the absolute best way to do this, but it might give you some idea of what sort of integration Swoosh could provide.

Instead of EmailView I set up a new component module and imported my templates. I moved my layout template into the same dir for convenience. I converted my templates to .heex and updated them to use new components like <.link>, but otherwise left them the same.

defmodule AppWeb.EmailHTML do
  use AppWeb, :html
  embed_templates "../templates/email/*"
end

I added a couple helper methods to my email module to render the heex templates into an HTML string that I could pass to Swoosh.

defmodule AppWeb.Emails.UserEmail do
  import Swoosh.Email

  defp render_with_layout(email, heex) do
    html_body(
      email,
      render_component(AppWeb.EmailHTML.layout(%{email: email, inner_content: heex}))
    )
  end

  defp render_component(heex) do
    heex |> Phoenix.HTML.Safe.to_iodata() |> IO.chardata_to_string()
  end

  def send_my_email(foo) do
    new()
    |> subject("Here's my email")
    |> render_with_layout(AppWeb.EmailHTML.my_email(%{foo: foo}))
  end
end

That's all it took, and it seems to be working well! I like the simplicity of having everything in components. If Swoosh provided some convenience helpers for the rendering piece, that would be pretty handy.

@princemaple
Copy link
Member

princemaple commented Feb 19, 2023

@iangreenleaf Yeah I've been wondering, after seeing the embed_templates/2, how much value Phoenix.Swoosh can still provide.

Thanks a lot for sharing!

@princemaple princemaple pinned this issue Feb 19, 2023
@silverdr
Copy link

silverdr commented Mar 6, 2023

@iangreenleaf Yeah I've been wondering, after seeing the embed_templates/2, how much value Phoenix.Swoosh can still provide.

I'd say more, but at the very least it can act as a compatibility layer. For large and complex email communication cases that's already a lot of value

@princemaple
Copy link
Member

I'd say more, but at the very least it can act as a compatibility layer. For large and complex email communication cases that's already a lot of value

Thanks. This can be accomplished by pinning an existing version. I was more wondering if I was to release a 2.0 of this package that is designed with phoenix 1.7 as a starting point, what more this package can provide to the users. 🤔

@silverdr
Copy link

silverdr commented Mar 6, 2023

I understand. I meant the 1.7+ and getting away with "Views" as this is probably what will have to happen anyway. Something that would keep the existing codebase work after migrating to a Phoenix version, which no longer has them in their current form.

Well, probably with only minor modifications at the top of each related file. Whether you find that worth the effort that's of course a different story.

Also, I am not sure if there's good enough support for multiple languages. I'd be happy to see something that simplifies picking a correct language variant of the template as well as an ability to build the final body from several templates. Can be for a multilingual message for example or for building larger messages from smaller building blocks

@princemaple
Copy link
Member

an ability to build the final body from several templates

Can be for a multilingual message for example or for building larger messages from smaller building blocks

With embed_templates macro, this is already handled by compiling bits and pieces into a module of functions and call them from your *.heex or even just *.eex files.

I'd be happy to see something that simplifies picking a correct language variant of the template

This is probably the only value left.

@preciz
Copy link

preciz commented Mar 8, 2023

@iangreenleaf thx for the code example, if somebody has the email layout in the new my_app_web/components/layouts folder:

AppWeb.EmailHTML.layout should be AppWeb.Layouts.email

defp render_with_layout(email, heex) do
  html_body(
    email,
    render_component(AppWeb.Layouts.email(%{email: email, inner_content: heex}))
  )
end

@princemaple
Copy link
Member

@preciz this can be different for every repo depending on the setup and when the project was generated.

@preciz
Copy link

preciz commented Mar 9, 2023

@preciz this can be different for every repo depending on the setup and when the project was generated.

You are right, I intended to bring it closer to somebody who is looking at a newly generated 1.7 Phoenix codebase, I edit my comment.

@florish
Copy link

florish commented Apr 6, 2023

Hi! Thanks for opening this issue, very helpful to read during a Phoenix 1.7 upgrade for a project using phoenix_swoosh.

In reply to this part of the conversation:

> I'd be happy to see something that simplifies picking a correct language variant of the template
This is probably the only value left.

I'd like to add that phoenix_swoosh is also very convenient for sending text and html versions of the same email.

The new embed_templates/2 function does support multiple formats, but a suffix must be added to distinguish text from html versions.

This leaves a bit more work to do compared to the automatic dual HTML/text format detection phoenix_swoosh provides when using an atom as template value in render_body/3.

@florish
Copy link

florish commented Apr 6, 2023

Also something to consider for a new view-less version: a fresh start under the name of swoosh_phoenix could be helpful to avoid possible confusion about this package being a core Phoenix component such as phoenix_template, phoenix_live_view, etc.

@princemaple
Copy link
Member

Also something to consider for a new view-less version: a fresh start under the name of swoosh_phoenix could be helpful to avoid possible confusion about this package being a core Phoenix component such as phoenix_template, phoenix_live_view, etc.

That is true. I sometimes think that too. (I didn't create this project)

@shibaobun
Copy link

shibaobun commented Apr 14, 2023

I also updated a small app to 1.7 wanting to drop the Phoenix.View dependency. MyAppWeb.EmailHTML contains my email templates, MyAppWeb.Layouts contains my root email template, and I used import Phoenix.Template to get render_to_string, to do something like this:

def generate_email("update_email", user, %{"url" => url}) do
  user
  |> base_email(dgettext("emails", "Update your email"))
  |> render_body(:update_email, %{user: user, url: url})
end

defp render_body(email, template, assigns) do
  html_heex = apply(EmailHTML, String.to_existing_atom("#{template}_html"), [assigns])
  html = render_to_string(Layouts, "email_html", "html", email: email, inner_content: html_heex)

  text_heex = apply(EmailHTML, String.to_existing_atom("#{template}_text"), [assigns])
  text = render_to_string(Layouts, "email_text", "text", email: email, inner_content: text_heex)

  email |> html_body(html) |> text_body(text)
end

And then my EmailHTML:

defmodule MyAppWeb.EmailHTML do
  use MyAppWeb, :html

  embed_templates "email_html/*.html", suffix: "_html"
  embed_templates "email_html/*.txt", suffix: "_text"
end

Edit: Updated from original approach because it was a good idea, and I even checked that it worked 😄

@princemaple
Copy link
Member

This thread will eventually shape the next version of the library 🙂

@ftes
Copy link

ftes commented Jun 15, 2023

To add mjml and text formats (adapted from above ☝️):

# email.ex
defmodule Email do
  import Phoenix.Template, only: [embed_templates: 2]

  embed_templates("templates/email/*.mjml", suffix: "_mjml")
  embed_templates("templates/email/*.text", suffix: "_text")
end

# user_notifier.ex
defmodule UserNotifier do
  import Swoosh.Email

  def welcome(user) do
    assigns = %{name: user.name}

    new()
    |> html_body_with_layout(Email.welcome_mjml(assigns))
    |> text_body_with_layout(Email.welcome_text(assigns))
  end

  defp html_body_with_layout(email, inner_content) do
    body =
      %{email: email, inner_content: inner_content}
      |> Email.layout_mjml()
      |> to_binary()
      |> to_html()

    html_body(email, body)
  end

  defp text_body_with_layout(email, inner_content) do
    body =
      %{email: email, inner_content: inner_content}
      |> Email.layout_text()
      |> to_binary()

    text_body(email, body)
  end

  defp to_binary(rendered), do: rendered |> Phoenix.HTML.Safe.to_iodata() |> IO.iodata_to_binary()
  defp to_html(mjml_binary), do: with({:ok, html} <- Mjml.to_html(mjml_binary), do: html)
end

@princemaple
Copy link
Member

@ftes that's pretty comprehensive! Cheers

@josevalim
Copy link
Contributor

Very good discussion folks. Just one quick addition, you can use Phoenix.Template.render_to_string in some of the cases above so you encapsulate the Safe.to_iodata conversion. :)

@josevalim
Copy link
Contributor

josevalim commented Aug 9, 2023

Here is another tip, you don't even need to define a module with templates. From Phoenix v1.7, you can use ~H directly, especially if you are not using layouts:

defmodule Hub.Notifier do
  use HubWeb, :html
  require Logger

  def deliver_invitation(org, inviter, email) do
    assigns = %{org: org, inviter: inviter, email: email}

    email_body = ~H"""
    <p>Hi <%= @email %>,</p>
    ...
    """

    deliver(email, "You're invited to join #{org.name}", email_body)
  end

  defp deliver(recipient, subject, body) do
    email =
      Swoosh.Email.new(
        to: recipient,
        from: {"Livebook Teams", "[email protected]"},
        subject: subject,
        html_body:
          body
          |> Phoenix.HTML.html_escape()
          |> Phoenix.HTML.safe_to_string()
      )

    case Mailer.deliver(email) do
      {:ok, _} ->
        {:ok, email}

      {:error, reason} ->
        Logger.warning("Sending email failed: #{inspect(reason)}")
        {:error, reason}
    end
  end
end

So I think there are strong arguments that perhaps this lib is no longer necessary indeed. :)

@princemaple
Copy link
Member

Cheers @josevalim. Maybe Phoenix can have an official comprehensive Notifier guide, then we can formally retire this lib :)

@josevalim
Copy link
Contributor

Good idea. If anyone would like to contribute one, feel free to PR one and ping me, I will be glad to review and guide its way in!

@josevalim
Copy link
Contributor

Here is an interesting article to keep the discussion going: https://andrewian.dev/blog/phoenix-email-defaults

@linusdm
Copy link

linusdm commented Jan 3, 2024

In addition to a dedicated notifier guide, it might be an idea to enhance the mix phx.gen.notifier mix task with an example of how you'd render the template using the ~H sigil directly.

@aoioioi
Copy link

aoioioi commented Jan 11, 2024

Something like this seems to do the trick.

defmodule PhxMail.Notifier do
  use PhxMail, :html
  import Swoosh.Email
  require EEx

  def welcome(assigns) do
    mjml = """
    <mjml>
    <mj-body>
      <mj-section padding-top="30px" padding-bottom="30px">
        <mj-column>
          <mj-text>Hello {{first_name}},</mj-text>
          <mj-text>Welcome!</mj-text>
        </mj-column>
      </mj-section>
    </mj-body>
    </mjml>
    """

    {:ok, html} = Mjml.to_html(mjml)

    template = html_to_heex(html)
    final_email = EEx.eval_string(template, assigns: assigns)

    new()
    |> to({assigns.first_name <> " " <> assigns.last_name, assigns.email})
    |> from({"ABC", "[email protected]"})
    |> subject("Welcome!")
    |> html_body(final_email)
  end

  defp html_to_heex(html) do
    ~r/{{\s*([^}^\s]+)\s*}}/
    |> Regex.replace(html, fn _, variable_name ->
      "<%= @#{variable_name} %>"
    end)
  end
end

@kevinschweikert
Copy link

defmodule MyApp.Notifier do
  import Swoosh.Email
  require EEx

  def welcome(assigns) do
    mjml = """
    <mjml>
    <mj-body>
      <mj-section padding-top="30px" padding-bottom="30px">
        <mj-column>
          <mj-text>Hello <%= @first_name %>,</mj-text>
          <mj-text>Welcome!</mj-text>
        </mj-column>
      </mj-section>
    </mj-body>
    </mjml>
    """

    {:ok, html} =  EEx.eval_string(mjml, assigns: assigns) |> Mjml.to_html()
    
    new()
    |> to({assigns.first_name <> " " <> assigns.last_name, assigns.email})
    |> from({"ABC", "[email protected]"})
    |> subject("Welcome!")
    |> html_body(html)
  end
end

@aoioioi you can use EEx directly without the Regex conversion and i don't think that use PhxMail, :html is necessary

@ruslandoga
Copy link

ruslandoga commented Dec 11, 2024

👋

While working on auto-generating text emails from HTML emails (apparently a text part is sometimes required by some relays) I remembered this issue and wondered, why not use Markdown for both? If there was a library like begriffs/mimedown in Elixir, then maybe it would've been possible to do something like this:

defmodule MyApp.Notifier do
  import Swoosh.Email

  def welcome(assigns) do
    %{html: html, text: text} = MyApp.Mimedown.render("welcome.md", assigns)
    
    new()
    |> to({assigns.first_name <> " " <> assigns.last_name, assigns.email})
    |> from({"ABC", "[email protected]"})
    |> subject("Welcome!")
    |> html_body(html)
    |> text_body(text)
  end
end

And editing Markdown templates would have been super easy with VSCode previews.

Thoughts?

@josevalim
Copy link
Contributor

Yeah, big fan of this approach. I introduced a similar style back in Rails a long long long time ago. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests