Blogs

Use Javascript In Your Template

By Leo Liu posted 03-03-2022 23:34

  

The new HTML template is quite flexible. With the merge fields and decorator functions, you can easily manipulate the data, to format, filter, group, transform, aggregate data. You can also use custom CSS to adjust the page layout,  look-and-feel, page break, etc.

However, there will always be use cases we can't support out of the box. You then need to use Javascript to extend the OOTB functionalities.

How To Add Javascript to Your HTML Template

To write Javascript in your template, you can:

  1. Add an HTML component to your template.

  1. Write HTML/Javascript in the HTML Content textarea input. For example:
<strong id='greeting'></strong>
<script>
  document.getElementById('greeting').innerText = 'Hello World';
</script>

Preview the template, you can see the Hello World on the generated PDF.

Pass Data To Your Javascript

In the context of templating, Javascript is not attractive without the ability to manipulate the data. Before telling the answer directly, It's important to understand how Javascript works in the context of template-based document generation.

 

When you edit an HTML template and add a snippet of Javascript in an HTML component,  think of the javascript as a Javascript template, which is part of the HTML template. And the template eventually produces Javascript code after the data binding phase.

For example, given the following template:

<div id='container'></div>
<script>
document.getElementById('container').innerText = "{{Invoice.InvoiceNumber}}";
</script>

After the data binding, the generated HTML will look like:

<div id='container'></div>
<script>
document.getElementById('container').innerText = "INV-00000101";
</script>

That's the way you pass data to your Javascript. It's easier to understand how it works to see it as a template-based code generation.


With this idea in mind, we can pass complex data to our javascripts, for example:

<label>Recent Total Balance:</label> <span id='recentTotal'></span>
<script>
var invoicesOfAccount = [
{{#Invoice.Account.Invoices}}
  {
    "id": "{{Id}}",
    "number": "{{InvoiceNumber}}",
    "balance": {{Balance}},
    "amount": {{Amount}},
    "invoiceDate": "{{InvoiceDate}}"
  },
{{/Invoice.Account.Invoices}}
];
var totalBalanceInLast30Days = invoicesOfAccount
    .filter(invoice => invoice.invoiceDate >= (new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)).toISOString().substr(0,10))
    .reduce((sum,inv) => sum + inv.balance, 0);
document.getElementById('recentTotal').innerText = totalBalanceInLast30Days;
</script>​

In the above example, we declare a variable to hold all invoices of the account, filter all the invoices which InvoiceDate is in the last 30 days, and then add up the balances. By data binding, the generated variable declaration will be like:

<script>
var invoicesOfAccount = [
  {
    "id": "2c92c8957d5847df017d63f1ef9277a2",
    "number": "INV00000051",
    "balance": 0.0,
    "amount": 8271.0,
    "invoiceDate": "2021-11-27"
  },
  {
    "id": "2c92c8957c12843e017c1512c80a0404",
    "number": "INV00000044",
    "balance": 0.0,
    "amount": 94210.0,
    "invoiceDate": "2021-10-08"
  },
  {
    "id": "2c92c895796534100179683f0f9f0515",
    "number": "INV00000028",
    "balance": 0.0,
    "amount": 108.0,
    "invoiceDate": "2020-06-01"
  },
  {
    "id": "8a90f3207e9bd34c017e9f7beaae0346",
    "number": "INV00000061",
    "balance": 27349.0,
    "amount": 27349.0,
    "invoiceDate": "2022-02-01"
  },
];
...
</script>​

Common Use Cases

With the ability to pass data to Javascripts, we can implement a lot of use cases that may or may not be supported out of the box. Here, we list some common use cases you might want to implement with Javascript.

Barcodes

Zuora provides a builtin function to generate barcodes, for example, you can do:

{{#Invoice}}
{{#Wp_Barcode}}
CODE_128
*3517{{InvoiceNumber}}80*
{{/Wp_Barcode}}
{{/Invoice}}

But currently, it only supports popular barcode types. See the article in the Zuora knowledge center for more information.

If you have needs to add barcodes that are not in the support list, for example, the Swiss QR code, like:

You don't have to wait until Zuora implements it, you can implement it by using Javascript.

The idea is simply to import a barcode JS library and pass data to the generator scripts.

See this blog post for details about how to add barcodes using Javascript.

Charts

At the time of writing, we still do not support adding charts like a bar chart, pie chart in the template. However, with Javascript support, you have the ability to implement it by yourself.

For example, you can create a bar chart by using Chart.js:

<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script>
<!-- container needs to use relative position -->
<div style="width: 100%;display: flex; justify-content: center;">
    <div class="chart-container" style="position: relative; height:200px; width:400px;">
        <div>
            <canvas id="myChart" style="margin: 0 auto;"></canvas>
        </div>
        <script>
            const ctx = document.getElementById('myChart').getContext('2d');
            const myChart = new Chart(ctx, {
                type: 'bar',
                data: {
                    labels: [{{#Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}"{{InvoiceDate}}",{{/Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}],
                    datasets: [{
                        label: 'Invoice Trend',
                        data: [{{#Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}{{Amount}},{{/Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}],
                        backgroundColor: [
                            'rgba(255, 99, 132, 0.2)',
                            'rgba(54, 162, 235, 0.2)',
                            'rgba(255, 206, 86, 0.2)',
                            'rgba(75, 192, 192, 0.2)',
                            'rgba(153, 102, 255, 0.2)',
                            'rgba(255, 159, 64, 0.2)'
                        ],
                        borderColor: [
                            'rgba(255, 99, 132, 1)',
                            'rgba(54, 162, 235, 1)',
                            'rgba(255, 206, 86, 1)',
                            'rgba(75, 192, 192, 1)',
                            'rgba(153, 102, 255, 1)',
                            'rgba(255, 159, 64, 1)'
                        ],
                        borderWidth: 1
                    }]
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    scales: {
                        y: {
                            beginAtZero: true
                        }
                    }
                }
            });
            // resize the canvas size
            myChart.canvas.parentNode.style.width = "400px";
            myChart.canvas.parentNode.style.height = "200px";
            </script>
    </div>
</div>

Preview the template, you will see a bar chart in the generate PDF like:


In the above code, the important lines are:

data: {
  labels: [{{#Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}"{{InvoiceDate}}",{{/Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}],
...
  datasets:[{
    data: [{{#Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}{{Amount}},{{/Invoice.Account.Invoices|SortBy(InvoiceDate,ASC)}}],
...
}],
...
}

For the `labels` property, we iterate a list section Invoice.Account.Invoices|SortBy(InvoiceDate,ASC), and construct an array of InvoiceDate, which is used to anchor the bar on the X-axis. And similarly, we can construct the dataset for the Y-axis.

Outgoing API Calls

With Javascript, you can fetch external data and use them in your template. For example, if you don't want to use either the Wp_Barcode or the Javascript library to generate barcode, instead, you have an API endpoint returning barcode image. With Javascript, you can fetch that image and put it in the template.

Here's a sample to issue outgoing requests:

<div id="main"></div>
<script>
fetch('https://api.github.com/orgs/zuora')
  .then(response => response.json())
  .then(data => {
    document.getElementById('main').innerHTML = JSON.stringify(data);
  })
  .catch(error => document.getElementById('main').innerHTML = error)
</script>

Caveats

  • Please note that the Javascript is executed at the PDF rendering phase, outgoing requests could significantly slow down the PDF generation, and probably end up with a timeout error.
  • The endpoint resources have to be CORS-enabled, otherwise, the requests will be blocked.

The HTML templates support outgoing requests through JavaScript. But Zuora doesn't encourage you to use it in production as outgoing requests will make the document generation process vulnerable. Use it at your discretion.

1 comment
132 views

Comments

08-25-2022 08:19

Problem

I would like to have the header show up on page 2 and further, but be removed on Page 1 when the react-pdf is generated. 

Actual
I have placed the header-row "Header Block" after the first page break (Top of the second page) instead of at the top of the template. 

Unfortunately, this approach still has the header display on all pages; which seems intentional, since that is what the footer does as well.

Expected
If we wanted the Header and/or Footer on every page (say for page number, then it would logically need to be placed in before a page break so it would show on that page and any subsequent page.

Optional Solutions

Could we get a toggleor input for each the Header and the Footer in left settings or page setup, that would display Block on page [2] and further? where the page number to start display would be a number input.

Start Header display on Page [ 2 ] 
Start Footer display on Page  [ 1 ]

Alternatively would there be some simple logic hook for the HTML header that might work for us to hook on to

{{^pageNumber === 1}} <div class="subPageHeaderContent"> {{InvoiceDetail}}{{/pageNumber === 1}} 

or if we had access to set a custom "CSS class" or data-attribute Header Row in the settings, then in a JS Block we could hook on to the first instance of that class and remove it before the pdf renders, possibly. I 'm not sure the limitations of the react-pdf implementation here.