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>