AJAX

When building your extension, you may find instances where you’d rather grab data and update sections of your form, rather than reload the whole page. This is how client-side rendered extensions always work, but you can do it in server-side rendered as well.

Note

A helpful overview of AJAX and Caliper is in the Ajax Support doc

Lets say you wanted to show the user the conversion rate you’re using to generate the table’s values. You could fetch this every time you reload. Or you could use Tecton’s requestExtensionData, an API made to help you make requests of your server asynchronously.

Place this q2-btn somewhere on your page:

<q2-btn id="getCurrencies" intent="workflow-secondary">Get Currency Data</q2-btn>

Replace the script section of your transaction_history.html.jinja2 with this new block.

<script>
    window.tecton.connected.then(function(tecton) {
        document.querySelector('q2-select').value = '{{current_selected_currency}}';

        const currencyButton = document.getElementById('getCurrencies')
        currencyButton.onclick = fetchCurrencies

        function fetchCurrencies(event) {
            tecton.sources.requestExtensionData({
                route: 'get_currency_data',
                body: {
                    'currency': document.querySelector('q2-select').value
                }
            })
            .then(response => {
                const data = response.data
                const currencyDiv = document.createElement('div')
                const currencyText = document.createTextNode(`USD to ${document.querySelector('q2-select').value}: ${data.conversionRate}`)
                currencyDiv.appendChild(currencyText)
                document.querySelector('#placeholder-converted-values').appendChild(currencyDiv);
            })
            .catch(error => {
                tecton.actions.showModal({
                    title: 'Error Fetching Currencies',
                    message: `We ran into this error while trying to fetch currencies: ${error.message}`,
                    modalType: 'error'
                })
            })
        }
    })
</script>

Just like last time, we’ve added an event listener to a button, this time called fetchCurrencies. Now we’re sending our server a currency value to retrieve at a route we haven’t defined yet, get_currency_value. We expect to get back a data object with a conversionRate entry. We make a div, add this data to a textNode and insert that text into the body. We get our currency rate back with no reload required!

As a best practice we’re also catching issues and providing the error in a modal. A more sophisticated approach would turn these errors into user readable format.

So now we need to give our python server the details so it knows how to handle our new request. Add this new method into your extension.py:

@ajax
async def get_currency_data(self):
    rates_dictionary = await self.get_currency_rates()
    selected_currency = self.form_fields.get('currency', 'USD')
    conversion_rate = rates_dictionary[selected_currency]

    return Success({
        'selectedCurrency': selected_currency,
        'conversionRate': conversion_rate
    })

This should look familiar. This time we’re getting the currency data, but instead of using it to convert our transactions, we’re just going to pass back the data we want. You’ll notice we’re using a couple new things: the Success class and an @ajax decorator. The @ajax decorator ensures we return the data in the proper format and the Success class sets our response to a 200 successful HTTP code.

Since these are new you’ll need to import them at the top of extension.py.

from q2_sdk.core.web import ajax
from q2_sdk.models.tecton import Success

And since we’ve added a new route, you should add our new route to your router:

router.update({
    'default': self.default,
    'submit': self.submit,
    'get_currency_data': self.get_currency_data
})

Press the new “Get Currency Data” button and find the latest currency rates at the bottom of your page. You’re almost ready to be a FOREX trader!

We have been persisting values in our stateless server by passing them back and forth and storing them in hidden inputs, but that isn’t always a good solution. Next, we’ll use our built in Caching to store data in a more sophisticated manner

For reference. Here’s our transaction_history.html.jinja2 file so far in its entirety:

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

<q2-select label="Currency" name="currency">
    {% for currency in currency_options %}
        {% if currency == current_selected_currency %}
            <q2-option value="{{currency}}" display="{{currency}}" selected>{{currency}}</q2-option>
        {% else %}
            <q2-option value="{{currency}}" display="{{currency}}">{{currency}}</q2-option>
        {% endif %}
    {% endfor %}
</q2-select>

<q2-btn id="getCurrencies" intent="workflow-secondary">Get Currency Data</q2-btn>
<input id="hidden_account_id_storage" name="account_id" value="{{current_account_id}}" type="hidden">

<div>
    <a href="#submit">Convert Currency</a>
</div>
<div id="placeholder-converted-values">

</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="text-right clr(const-stoplight-success)">+{{transaction.display_amount}} {{current_selected_currency}}</td>
                {% else %}
                    <td class="text-right">-{{transaction.display_amount}} {{current_selected_currency}}</td>
                {% endif %}
            </tr>
        {% endfor %}
    </tbody>
    </table>
    <script>
        window.tecton.connected.then(function(tecton) {
            document.querySelector('q2-select').value = '{{current_selected_currency}}';

            const currencyButton = document.getElementById('getCurrencies')
            currencyButton.onclick = fetchCurrencies

            function fetchCurrencies(event) {
                tecton.sources.requestExtensionData({
                    route: 'get_currency_data',
                    body: {
                        'currency': document.querySelector('q2-select').value
                    }
                })
                .then(response => {
                    const data = response.data
                    const currencyDiv = document.createElement('div')
                    const currencyText = document.createTextNode(`USD to ${document.querySelector('q2-select').value}: ${data.conversionRate}`)
                    currencyDiv.appendChild(currencyText)
                    document.querySelector('#placeholder-converted-values').appendChild(currencyDiv);
                })
                .catch(error => {
                    tecton.actions.showModal({
                        title: 'Error Fetching Currencies',
                        message: `We ran into this error while trying to fetch currencies: ${error.message}`,
                        modalType: 'error'
                    })
                })
            }
        })
    </script>
</div>