maizzle / framework

Quickly build HTML emails with Tailwind CSS.
https://maizzle.com
MIT License
1.21k stars 48 forks source link

How to loop over arrays provided after build time? #1235

Closed RafaelZasas closed 5 months ago

RafaelZasas commented 5 months ago

Is it possible to have each loops that are not strictly defined at build time?

I am using the templates along with firebase trigger email extension which allows me to pass data to the template via handlebars.

I am trying to send order confirmation emails with a list of all the order items:

              <each loop="item, index in @{{orderItems}}">
                <table class="w-full">
                  <tr>
                    <td class="w-[72px] align-top">
                      <img
                        src="{{item.image}}"
                        alt="{{item.name}}"
                        width="48"
                      />
                    </td>
                    <td
                      class="w-[400px] text-left align-top text-base text-gray-900 sm:w-auto"
                    >
                      <p class="m-0 mb-2 font-bold">{{item.name}}</p>

                      <p class="leading-5.5 m-0 mb-2">{{item.description}}</p>

                      <p class="m-0 leading-3 text-gray-500">
                        Qty. {{item.quantity}}
                      </p>

                      <p class="leading-5.5 m-0 mt-2 text-gray-500">
                        PROMO20 (-$20)
                      </p>
                    </td>
                    <td class="text-base/5.5 w-[88px] text-right align-top">
                      <p class="m-0 mb-2 text-gray-700">
                        <strike>$99</strike>
                      </p>
                      <p class="m-0 font-bold text-gray-900">R{{item.price}}</p>
                    </td>
                  </tr>
                </table>

                <x-spacer height="24px" />
              </each>

The @{{orderItems}} variable is provided to the extension (which uses nodemailer) inside of a object named data.

I am able to display everything else- including the payment info, the date, the name and any other attribute I include in the data object, and reference in the template via @{{ propertyName }} .

The issue is that when I build the templates, everything inside of the each tag simply disappears...

Is this because the maizzle framework cant evaluate the @{{orderItems}} array at build time and defaults to ignoring the loop entirely?

Surely this is a common issue? How do other people send order confirmation emails or receipts with a list of items that the customer purchased.

RafaelZasas commented 5 months ago

Essentially I need the each loop to be dynamic. Its impossible to know how many items are in the order before the order it placed. The built HTML needs to be able to render n number of orderItems- where n is the length of an array, which is provided by handlebars syntax.

Listing order items must be an extremely common practice. I just cant find where in the documentation it explains how to do it.

cossssmin commented 5 months ago

Help me understand, why the @ in @{{orderItems}}?

{{ }} expressions with a @ in front will be ignored in Maizzle so the loop would output nothing:

https://maizzle.com/docs/expressions#ignoring

You'd need to pass an actual array/object data to the loop attribute there, is that not possible in your case?

RafaelZasas commented 5 months ago

@cossssmin I am ignoring the maizzle interpreted handlebars on purpose.

The handlebars need to be present in the final built output because the data is dynamic for each order. The html needs to be hydrated dynamically.

Is the solution to run npm run build to build the outputs with the provided customer information and order data for each and every email sent?

RafaelZasas commented 5 months ago

@cossssmin I guess I may be misunderstanding the correct way to use maizzle. How am I supposed to be generating dynamic emails based on user interactions?

Currently, the firebase extension flow is as follows: I add a document to the mail collection in firestore, with this data structure:

export type MailData = {
  to?: string | string[]
  from?: string
  replyTo?: string
  message?: Message
  template?: {
    name: string
    data?: { [key: string]: any }
  }
  toUids?: string[]
  cc?: string[]
  ccUids?: string[]
  bcc?: string[]
  bccUids?: string[]
  headers?: any
}

type Message = {
  subject?: string
  text?: string
  html?: string
  messageId?: string
  amp?: string
  attachments?: Attachment[]
}

That then gets queued for sending. If a template is used instead of message, then the data object in the template is injected into handlebars.

What I am doing, is copying the build outputs from maizzle, and pasting them into the templates.

That is why I need the handlebars

cossssmin commented 5 months ago

Have a look at using Maizzle programmatically:

https://maizzle.com/docs/api

You'd pass your template.data inside the maizzle config that you provide to the render method, i.e.:

const Maizzle = require('@maizzle/framework')

const options = {
  maizzle: {
    locals: {
      order: template.data
    }
  },
}

Maizzle
  .render(`html string`, options)
  .then(({html}) => console.log(html))

With that setup you can loop over order directly:

<each loop="item, index in order">...</each>

Mind the Tailwind config caveat, you need to pass it otherwise you'll get the default Tailwind config which doesn't play nice in emails. You can just read our tailwind.config.js and provide it in the options, something like:

const options = {
  maizzle: {
    locals: {
      order: template.data
    }
  },
  tailwind: {
    config: JSON.parse(fs.readFileSync('path/to/tailwind.config.js'))
  }
}
RafaelZasas commented 5 months ago

@cossssmin

Where would this code be?

const Maizzle = require('@maizzle/framework')

const options = {
  maizzle: {
    locals: {
      order: template.data
    }
  },
}

Maizzle
  .render(`html string`, options)
  .then(({html}) => console.log(html))

I dont have a "backend" in this stack. Firebase abstracts that away. Ie. There is no NodeJS api that I control, other than the Frontend react I suppose.

It would seem slightly insane to bundle the entire maizzle templates directory with the front end and using the maizzle cli to build the html.

RafaelZasas commented 5 months ago

@cossssmin is Maizzle.render the process being used to generate the dist outputs when npm run build is executed?

If I were to completely bypass the build step, and instead use the file in the src/templates directory, how would I have access to the components such as the header and footer? The html string would reference components, how would the Maizzle.render function know where to find those?

cossssmin commented 5 months ago

Yeah to use it programmatically you need a Node backend, no other way around it.

If you're just doing npm run build locally you need to provide the order data in some way to Maizzle so it can loop over it. So if at first you're @-ignoring the {{orderItems}} expression, the only option I see is that you re-run the build once you have the order data from Firebase and can provide it in the config.

You could probably use the afterBuild event to rebuild everything again with the order data included in the config, but yeah it's doing double work and could even lead to unexpected results from the Transformers applied (I'd disable all of them if rebuilding).

This kind of scenario is exactly what the API/programmatic is for - get the data, render the template with it.

cossssmin commented 5 months ago

The render method compiles your source HTML, i.e. it evaluates expressions and replaces component tags with their actual HTML. It doesn't output files, it just returns the compiled HTML string.

RafaelZasas commented 5 months ago

@cossssmin Would this work?:

cossssmin commented 5 months ago

It would but you're doing the work twice. If you have the variables like {{orderItems}} available when running the serverless function you could just use Maizzle programmatically to compile a template with those variables as context.

Flow would be like:

RafaelZasas commented 5 months ago

@cossssmin I see, however I'm still not sure how Maizzle.render would stitch the components in the serverless function. I cant include the entire maizzle framework, with all the components, and all the templates in the serverless function. The directory (maizzle_templates) was cloned from a package I purchased which includes many different email templates, and is 172MB. That is far too large for the serverless function

cossssmin commented 5 months ago

Yes, for the components right now only file system is supported, meaning that it scans them on disk. So in the maizzle config you'd provide, you'd need to also configure the paths where the components files live (and your serverless env must support file system, otherwise it can't work).

So you'd just need the component files, not the entire project. Btw it's 172MB probably because of node_modules, which you'd ignore anyway.

In the future we're looking into adding support for defining components right in the component config, so instead of scanning the file system it would use them from the config object - this will make it usable in more scenarios, including the browser. But it will take time, right now there's like one person working on this - me.

cossssmin commented 5 months ago

If your env doesn't support the file system, another idea would be to just inline the components. Especially if you're only using stuff like <x-spacer> or <x-button>, this wouldn't be complicated to do 👍

RafaelZasas commented 5 months ago

@cossssmin I appreciate your work. I would be happy to support you by purchasing your templates when it becomes available.

I love maizzle for the fact that the html is email client specific, and I can create emails with components. I would however like the option to inject variables into the full html string myself, using golang html/template or whatever html parser I choose. being confined to nodejs isnt ideal- I simply hate server side javascript and avoid it like the plague.

I love maizzle and have been using it for a few years now. This is the first time I needed to iterate over an array of objects in an email with maizzle templates.