DEV Community

Nathan Cook
Nathan Cook

Posted on • Edited on

Fully featured report generation in Node with the JSReports rendering core

(Check out this repo to get an idea of what's possible)

If you're interested in a free, self-hosted NodeJS report generation solution that doesn't compromise on templating functionality, you should check out JSReport, specifically the functionality one can build using the jsreport 'rendering core'.

JSReport is open-source, and unlike many SaaS providers in this space, they do not gatekeep ANY of their product's core functionality behind tiered subscription pricing. While JSReport does charge for their web-based 'Template Studio' and SaaS offerings, the report generation framework that powers these products is open source and free to use however you want.

Let's get started with a basic example.

Open up your terminal (I'm assuming linux, no promises if you're in Windows-land) and run the following:

git clone https://github.com/moofoo/basic-jsreport-core-example.git && \
cd basic-jsreport-core-example && \
yarn

Enter fullscreen mode Exit fullscreen mode

First, lets look at the package.json

{
  "name": "basic-jsreport-core-example",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "@jsreport/jsreport-core": "^3.11.4",
    "@jsreport/jsreport-docx": "^3.7.1",
    "@jsreport/jsreport-handlebars": "^3.2.1",
    "@jsreport/jsreport-unoconv": "^3.0.1",
    "handlebars": "^4.7.7"
  }
}

Enter fullscreen mode Exit fullscreen mode

@jsreport/jsreport-core is the "minimalist jsreport rendering core", on to which we'll add 'extensions' (plugins, what have you), to build up the functionality we want.

JSReport categorizes extensions into three categories: Engines, Recipes, Everything Else

  1. Engines - These are templating languages, i.e. handlebars, ejs, etc. The current example uses handlebars, by way of the @jsreport/jsreport-handlebars extension. For any given Engine you will also need the actual templating language in your dependencies - handlebars@4.7.7 in this case.

  2. Recipes

    "A recipe represents the technique used to print the document. It can be an HTML to pdf conversion, DOCX rendering, and others."

The current example employs the docx recipe, so we have the @jsreport/jsreport-docx extension as a dependency.

3, Everything Else - for instance, this repo uses the @jsreport/jsreport-unoconv extension to do PDF conversion via unoconv.

Now let's look at index.js. Here's that file:

const { readFileSync, writeFileSync, unlinkSync } = require("fs");
const templateData = require('./data.json');

// remove existing report files, if they exist
try {
  unlinkSync("./report.docx");
  unlinkSync("./report.pdf");
} catch (err) {
  console.log("no report files");
}

// Create jsreport instance
const jsreport = require("@jsreport/jsreport-core")();

// Add plugins: docx recipe, handlebars engine and unoconv extension for pdf conversion
jsreport.use(require("@jsreport/jsreport-docx")());
jsreport.use(require("@jsreport/jsreport-handlebars")());
jsreport.use(require("@jsreport/jsreport-unoconv")());

(async () => {
  // Initialize jsreport instance
  await jsreport.init();

  // Read template.docx, base64 encoded
  const content = readFileSync("./template.docx", {encoding: "base64"});

  // Render a .docx report from template
  const resultDocx = await jsreport.render({
    template: {
      content: "",
      recipe: "docx",
      engine: "handlebars",
      docx: {
        templateAsset: {
          content,
          encoding: "base64",
        },
      },
    },
    data:templateData
  });

  // Write report.docx to disk
  await writeFileSync("./report.docx", resultDocx.content, {encoding: "utf-8"});
  console.log("report.docx created");

  // Render a .pdf report from template using unoconv extension
  const resultPdf = await jsreport.render({
    template: {
      content: "",
      recipe: "docx",
      engine: "handlebars",
      docx: {
        templateAsset: {
          content,
          encoding: "base64",
        },
      },
      unoconv: {
        format: "pdf",
        enabled: true,
      },
    },
    data: templateData
  });

  // Write report.pdf to disk
  await writeFileSync("./report.pdf", resultPdf.content, "binary");
  console.log("report.pdf created");
})();

Enter fullscreen mode Exit fullscreen mode

Let's go over the JsReport-relevant lines. We start with some initialization boilerplate:

// Create jsreport instance
const jsreport = require("@jsreport/jsreport-core")();

// Add plugins: docx recipe, handlebars engine and unoconv extension for pdf conversion
jsreport.use(require("@jsreport/jsreport-docx")());
jsreport.use(require("@jsreport/jsreport-handlebars")());
jsreport.use(require("@jsreport/jsreport-unoconv")());

Enter fullscreen mode Exit fullscreen mode

The next lines initialize the instance given the added extensions and gets the template file:

  // Initialize jsreport instance
  await jsreport.init();

  // Read template.docx, base64 encoded
  const content = readFileSync("./template.docx", {encoding: "base64"});

Enter fullscreen mode Exit fullscreen mode

Note the base64 encoding.

At this point, the jsReport instance is ready to render docx (or pdf) reports from .docx templates:

// Render a .docx report from template
  const resultDocx = await jsreport.render({
    template: {
      content: "",
      recipe: "docx",
      engine: "handlebars",
      docx: {
        templateAsset: {
          content,
          encoding: "base64",
        },
      },
    },
    data:templateData
  });

  // Write report.docx to disk
  await writeFileSync("./report.docx", resultDocx.content, {encoding: "utf-8"});
  console.log("report.docx created");
Enter fullscreen mode Exit fullscreen mode

Let's go over the options passed to jsreport.render, specifically the template config:

content: "",

  • While the content param isn't used here, JsReport will throw an error if it is undefined. An empty string suffices.

recipe: "docx"

  • This sets the render 'recipe'. Regarding naming conventions:

"The convention is that jsreport repository extension starts with jsreport-xxx, but the extension real name and also the recipes or engines it registers excludes the jsreport- prefix. This means if you install extension @jsreport/jsreport-handlebars the engine's name you specify in the render should be handlebars."

Accordingly, @jsreport/jsreport-docx transmutes to 'docx', here.

engine: "handlebars"

  • same story
docx: {
  templateAsset: {
    content,
    encoding: "base64",
  },
},

Enter fullscreen mode Exit fullscreen mode

content is just the template.docx file, base64 encoded, and encoding informs the render method about that encoding.

The next jsreport.render call is identical to the previous one, with one addition to the template options:

 unoconv: {
    format: "pdf",
    enabled: true,
 },

Enter fullscreen mode Exit fullscreen mode

So, when this render runs, a PDF will be created, rather than a .docx file. Note that enabled is assumed to be false if undefined, so it must be set to true here.

Using whatever document viewer you have handy, check out whats in template.docx. This is just an invoice example I pulled from the JSReport Playground

Ok, let's run this thing:

node index.js
Enter fullscreen mode Exit fullscreen mode

Assuming nothing goes horribly wrong, you should now have two report files in the repo root, report.pdf and report.docx.

--------------------

While the code presented here is nothing fancy, the real magic is how this extremely simple setup gives you full access to JSReport's robust templating features, for example:

And that's just for .docx based templates.

That's all for now!

NestJS + JSReport integration will be covered in a future article, however the repo for that article is already complete: https://github.com/moofoo/nestjs-jsreport-examples

Top comments (0)