Templates

The Caliper SDK can return custom HTML that you define. We call this concept ‘Templates’.

Content is generated using the Jinja2 Python templating framework, which allows easy substitutions of Python values into blocks of HTML or text. Many if not most of the markup you need can be constructed with Tecton components.

Here is a jinja template for the transaction history page. In the previous step, we created a file in AccountDashboard/templates called transaction_history.html.jinja2, which contains the following:

<div>
    <a href="#default">Back to Accounts</a>
</div>

<div class="table-wrapper">
    <table>
    <thead>
        <tr>
        <th width="100">
            Date
        </th>
        <th>
            Description
        </th>
        <th class="text-right">
            Amount
        </th>
        </tr>
    </thead>
    <tbody>
        {% for transaction in transactions %}
            <tr>
                <td>{{transaction.display_date}}</td>
                <td>{{transaction.display_description}}</td>
                {% if transaction.is_credit %}
                    <td class="clr(const-stoplight-success)">+{{transaction.display_amount}}</td>
                {% else %}
                    <td>-{{transaction.display_amount}}</td>
                {% endif %}
            </tr>
        {% endfor %}
    </tbody>
    </table>
</div>

There’s a lot going on here. We’ve laid out a basic HTML structure.

First, we demonstrate how to create an internal link in a custom template: by setting “#default” as our link’s href, the router will know to send the user back to the default route if this link is clicked.

For our table rows, the for helper iterates over the transaction models we will pass in and pulls out properties to display. Using the if helper, it can evaluate whether a transaction is a credit, and display it colored green if so.

When we declare our Q2Form, we inject this template (with our transaction models inserted). Here is our submit method:

async def submit(self):
    params_obj = GetAccountHistoryById.ParamsObj(
        self.logger,
        self.form_fields['account_id'],
        '',
        hq_credentials=self.hq_credentials
    )

    hq_response = await GetAccountHistoryById.execute(params_obj)

    transaction_models = []

    for transaction in hq_response.result_node.Data.AllHostTransactions.Transactions:
        display_date = str(transaction.PostDate)
        display_amount = str(transaction.TxnAmount)
        is_credit = (transaction.SignedTxnAmount > 0)
        display_description = str(transaction.Description)

        transaction_models.append({
            'display_date': display_date,
            'display_amount': display_amount,
            'is_credit': is_credit,
            'display_description': display_description
        })

    template = self.get_template(
        'transaction_history.html.jinja2', {
            'header': "Transaction History",
            'transactions': transaction_models,
            'current_account_id': self.form_fields['account_id'],
        }
    )

    html = self.get_tecton_form(
        "Transaction History",
        custom_template=template,
        hide_submit_button=True
    )

    return html
    

Our hq response object isn’t ideal for display, so we’re iterating through the transactions and creating a “model” (a simple dictionary) containing only the data we want to display. For now, we just convert our desired properties into strings and determine whether a transaction is a debit or credit. As each model is created, we add it to a list. This list is what the jinja for helper will loop through to make a tables row for each transaction.

In our form instantiation, we pass our html as custom_template and tell Q2Form not to bother rendering its default submit button– we have something special in mind for this page that we will implement in the next few steps.

Load up your extension, and you can see that our built-in RequestHandler method self.get_template() found our HTML file, inserted the necessary data into it, and returned the complete HTML for Q2Form to render.

Note

It might seem simple, but Jinja is a deceptively powerful tool, and well worth studying if you wish to craft complex extensions.

Here are the complete Jinja2 docs.

Input Validation

We can harness that template power with Tecton to upgrade our interface. What if, on the index page, you wanted to tell the user that they have entered an account number that doesn’t exist? How would you go about doing that? You could add some validation to the q2-input. We’ve got the account numbers on hand here to check against.

Add this to the bottom of your index.html.jinja2 file:

<script>
    const accounts = {{ accounts|tojson }}
    const accountNumbers = accounts.map(account => account.account_num)
    let inputField = document.getElementById('account_id')
    inputField.onblur = checkForErrors

    function checkForErrors(event) {
        if (!accountNumbers.find(accountNumber => accountNumber == parseInt(event.target.value))){
            inputField.errors = ["That's not a real account number. Please try again."]
        } else {
            inputField.errors = undefined
        }
    }
</script>

What are we doing here? We injected our accounts into the template as an object using jinja’s tojson filter. Then we’re mapping over the array of objects to pull out a new array of just account numbers. We’re adding an event handler to the onBlur event for the input. And finally we’re checking to see if the account number returned in the event object matches anything we’ve got in our list.

Now enter an account number into the input that isn’t in the list and click elsewhere on the page. The input turns red and adds an indicator icon. Now click back into it. You can see we’ve added feedback for the user. You just added input validation!

Tecton components translate shadowDOM events into lightDOM events so that we can interact with them, which is why you can still work with blur events for that input inside the q2-input. Tecton also handles styling components like the platform they’re in and takes into account the FI’s themes (style files created by the FI) so your forms always fit aesthetically.

Important

You should never inject user-inputted data that you haven’t sanitized into your pages. Failing to sanitize user input before adding it to your pages can open you up to cross-site scripting attacks. Our data for this code came from Q2, so it’s safe to work with.

There are other approaches of course. You could, and should, validate when form data is sent to your server. Or you could avoid this altogether with an input that restricts choices, like a q2-select. We can also improve on this with more feedback.

Note

For more details on how to construct great forms, check out the Tecton docs page on forms

Form Validation

In the last step we implemented input validation on our main page. Now that we know how to hide the submit button, we can also implement our own submit button and add form validation.

We want to make it so that when a user submits the form we check the form fields for errors. If there’s an error, we want to stop them submitting the form and give them feedback, so we’ll use a modal. We’ll need Tecton’s capabilities!

But first we’re going to need more control over the submit button, so we’ll hide the standard one and replace it with our own. Update our default method to this:

async def default(self):
    account_models = []
    for account in self.account_list:
        account_models.append({
            'account_num': account.host_acct_id,
            'account_name': account.product_name,
            'account_balance': account.balance_to_display
        })

    template = self.get_template('index.html.jinja2', {
        'accounts': account_models
    })

    html = self.get_tecton_form(
        "AccountDashboard",
        custom_template=template,
        routing_key="submit",
        hide_submit_button=True
    )
    return html

Just like with our transactions page, the submit button is removed. So we need to add that back. We’ll use a q2-btn. Add this snippet to the bottom of your index.jinja2:

<q2-btn intent="workflow-primary" onclick="validateForm(event)">Get Transaction History</q2-btn>

Now we’ll wrap our script in the tecton connect function, which returns a promise. In the .then method we’ll take the object the resolved promise passes us, which has Tecton’s actions and sources, and use it to call actions.showModal after checking for errors on any of the inputs. If there are no errors, we’ll use the submitForm function provided by the SDK to send the data to the server.

<script>
    window.tecton.connected.then(function(tecton) { 
        const accounts = {{ accounts|tojson }}
        const accountNumbers = accounts.map(account => account.account_num)
        let inputField = document.getElementById('account_id')
        inputField.onblur = checkForErrors

        function checkForErrors(event) {
            if (!accountNumbers.find(accountNumber => accountNumber == parseInt(event.target.value))){
                inputField.errors = ["That's not a real account number. Please try again."]
            } else {
                inputField.errors = undefined
            }
        }

        window.validateForm = function validateForm(event) {
            let inputs = Array.from(document.querySelectorAll('q2-input'))
            const form = document.querySelector('form')
            
            if (inputs.find(input => input.errors)){
                tecton.actions.showModal({
                    title: 'Form has Errors',
                    message: 'Please correct the fields with errors, indicated by a red triangle.',
                    modalType: 'error'
                })
            } else {
                submitForm()
            }
        }
    });
</script>

You can see we’re using a different method of attaching an event handler: using the element’s attributes. Since we’re using that approach, and since we’re working inside a then function which has its own scope, we need to attach our validation function to the window, aka the global scope, manually.

Note

In SSR extensions we add it to the global scope for you. In CSR you’ll import the library yourself and handle its storage and distribution according to your chosen library’s normal patterns.

Note

Tecton makes generous use of Promises. If you’re unfamiliar with that API, you should brush up on it. Javascript.info has a chapter on Promises that’s a good read.

Let’s test it out! Navigate to the index page, add a non-existent account number and press “Get Transaction History”. Boom! A modal and no submitted form. Form validation that fits the platform by using Tecton.

Next, we will explain how the Caliper SDK handles serving static assets.