Search code examples
node.jsexpresshandlebars.jsnodemailer

Nodemailer Express Handlebars dynamic email, Iterator not rendering data


I have an eCommerce solution and I want to send an email when an checkout is completed and an order is created. The problem is everything works and the template engine run the handlebars file. But when the mail is received, dynamic data passed to the file will not render.

This is my controller

const getOrderCheckout = asyncHandler(async (requestObject, responseObject) => {
  const { id } = requestObject.params;
  const orderCheckedout = await Order.findOne({
    "paymentIntent.transactionId": id,
  })
    .populate({
      path: "product.product",
      populate: {
        path: "brand",
        model: "Brand",
      },
    })
    .populate("orderBy");
  if (orderCheckedout) {
    const data = {
      to: orderCheckedout.orderBy.email,
      subject: "Order sucessfully made",
    };
    const assets = orderCheckedout.product;
    checkoutMailer(data, assets, (status, message) => {
      responseObject.status(status).json({ message: message });
    });
    responseObject.status(200).json(orderCheckedout);
  } else
    responseObject
      .status(404)
      .json({ message: "Could not find any orders, please try again" });
});

So a data like this is sent to nodemailer

[
  {
    product: {
      _id: new ObjectId("644d30d460b928dd60536b05"),
      title: 'Staedtler Triplus 0.3mm Pen',
      slug: 'office-supplies-staedtler-triplus-0.3mm-pen',
      price: 29,
      category: new ObjectId("6425ebbce7b7b9989c638f11"),
      brand: {name:"Staedtler"},
      quantity: 88,
      sold: 12,
      images: [{image:"<image path>")}],
      tags: [Array],
      totalRatings: '0',
      ratings: [],
      createdAt: 2023-04-29T14:59:32.318Z,
      updatedAt: 2023-06-10T12:51:31.736Z,
      __v: 1
    },
    quantity: 1,
    color: 'Blue',
    _id: new ObjectId("648471b1a1db266d6b1e8f51")
  }
]

This is nodemailer configuration to send the email

const checkoutMailer = asyncHandler(
  async (data, assets, callback, _requestObject, _responseObject) => {
    // create reusable transporter object using the default SMTP transport
    let transporter = nodemailer.createTransport({
      host: process.env.SENDMAIL_HOST_SERVER,
      port: 587,
      secure: false, // true for 465, false for other ports
      auth: {
        user: process.env.SENDMAIL_ACCOUNT_ID, // generated ethereal user
        pass: process.env.SENDMAIL_AUTH_STRING, // generated ethereal password
      },
    });
    const handlebarOptions = {
      viewEngine: {
        extName: ".handlebars",
        partialsDir: path.resolve("./public/template"),
        defaultLayout: false,
      },
      viewPath: path.resolve("./public/template"),
      extName: ".handlebars",
    };

    transporter.use("compile", hbs(handlebarOptions));
    // send mail with defined transport object
    let mailOptions = {
      from: '"Kelvin Kabute 👻" <skipgh@gmail.com>', // sender address
      to: data.to, // list of receivers
      subject: "Hello ✔ " + data.subject, // Subject line
      template: "order",
      context: assets,
    };

    transporter.sendMail(mailOptions, function (error, info) {
      if (error) {
        console.log(error);
      } else {
        console.log("Email sent: " + info.response);
        let reqStatus, message;
        if (info.accepted) {
          reqStatus = 100;
          message =
            "Message sent successfully with order information, please check both spam and inbox";
        } else {
          reqStatus = 417;
          message =
            "System error, message not sent please contact us on 0800-2232-222";
        }
        callback(reqStatus, message);
      }
    });
  }
);

This is my handlebars file and this is where my issue is. From {{#each assets}} does not show in the email sent.

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      .heading { font-size: 22px; color: #a2ce2c; } .banner { width: 300px;
      height: 80px; } .item-image { width: 90px; }
    </style>
  </head>
  <body>
    <div>
      <p class="heading">Order Items Being Processed</p>
      <img
        src="https://static.vecteezy.com/system/resources/thumbnails/005/021/047/small/illustration-land-or-ground-shipping-carrier-delivery-service-has-motorcycle-van-and-cargo-ship-yellow-or-orange-tones-design-for-banner-website-decorations-app-advertising-parcel-vector.jpg"
        alt="..."
      />
      {{#each assets}}
        <table>
          <tr>
            <th>Image</th>
            <th>Brand</th>
            <th>Item</th>
            <th>Quantity</th>
            <th>Unit Price</th>
          </tr>
          <tr>
            <td>
              {{#with this.product.images}}
                <img class="item-image" src={{this.0.image}} alt="..." />
              {{/with}}
            </td>
            <td>{{this.product.brand.name}}</td>
            <td>{{this.product.title}}</td>
            <td>{{this.quantity}}</td>
            <td>{{this.product.price}}</td>
          </tr>
        </table>
      {{/each}}
    </div>
  </body>
</html>

This is the email received enter image description here

Please help me resolve this


Solution

  • First of all I have made some hefty mistakes.

    1. I am supposed to give a name to the array being pass to context inside nodemailer configuration
    let mailOptions = {
          from: '"Kelvin Kabute 👻" <skipgh@gmail.com>', // sender address
          to: data.to, // list of receivers
          subject: "Hello ✔ " + data.subject, // Subject line
          template: "order",
          context: { assets },
        };
    

    ES6 object notation allow to use a single name to represent key/value pair so the name I gave it is assets

    2.assets is not a valid json object since its data direct from mongoose and the data has not yet been converted to json. Inside the controller const assets = orderCheckedout.products is replaced by

    const assets = orderCheckedout.product.map((item) => {
          return {
            title: item.product.title,
            brand: item.product.brand.name,
            price: item.product.price,
            image: item.product.images[0].image,
            quantity: item.quantity,
          };
        });
    

    Lastly and article I found online Send emails with Handlebars, nodemailer and Gmail Shared some light on how to compose my handlebars template. Each day we grow and sometimes we really make mistakes but we pick up and move on.