Using JS/Python Contexts in ActionKit¶
General Idea¶
We needed a way to do JS embedding of AK code, including user recognition, validation, etc. Briefly the way we do that is:
- A client loads a static page. That page loads /samples/actionkit.js and has an action form (usually named 'act').
- actionkit.js defines a bunch of JavaScript functions and attributes in an actionkit object, mostly in actionkit.forms and actionkit.utils.
- After the <body> tag, the client’s HTML calls actionkit.forms.initPage().
- After the </form> tag on their action form, the client needs to call actionkit.forms.initForm(‘act’).
- initForm inserts a <script> element in the page pointing to some server side code (/context/).
- The server side code returns JavaScript in the format callback_function (JSON data).
- The javascript callback function runs, and does things like “not bob?”, prefilling, and validation. It also processes ak-templates, which look like <script type=”text/ak-template”>[% foo %]</script>
Even more simply: the page loads Javascript, which loads other JavaScript that has dynamic info from the server. It's AJAX-y (JSONP is the buzzword for it), though the asynchronous calls are made in a cascade on page load and page submit rather than in response to particular user events like key presses or mouseovers. We use JSONP rather than straight AJAX so client forms can pull in context from any site, not just from pages at ActionKit (AJAX has to be same-domain).
Example Usage¶
db_templates/Original/lte.html
{% extends "./wrapper.html" %}{% load actionkit_tags %}
{% block script_additions %}
<script type="text/javascript">
function toggleChooser(on) { ... }
function countWords(textarea) { ... }
function commify(n) { ... }
function abbreviate(word, maxLength) { ... }
$(window).load( function() { ... }
$("input[name=media_target]").change(function() { toggleChooser(false) } );
</script>
{% endblock %}
{% block content %}
<form class="ak-form" name="act" method="POST" action="/act/" accept-charset="utf-8">
<input type="hidden" name="page" value="{{ page.name }}">
<div class="ak-grid-row">
<div class="ak-grid-col ak-grid-col-12-of-12">
<h2>{{ page.title }}</h2>
</div>
</div>
<div class="ak-grid-row">
<div class="ak-grid-col ak-grid-col-6-of-12">
{% if page.custom_fields.featured_image %}
<img class="ak-featured-img" src="{{page.custom_fields.featured_image}}">
{% endif %}
<div class="ak-styled-description ak-text-expander">
{% include_tmpl form.introduction_text %}
</div>
<a href="#" class="ak-read-more ak-mobile" data-lines="10">Read more</a>
<div id="lte-prelim"></div>
<div id="user_info_prompt">
</div>
<div id="lte-help"></div>
<script type="text/ak-template" for="lte-help">
<ul>
{% if form.talking_points %}
<li>
<div class="lte-help-head">
Talking Points
</div>
<div>
{% include_tmpl form.talking_points %}
</div>
</li>
{% endif %}
{% if form.writing_tips %}
<li>
<div class="lte-help-head">
Writing Tips
</div>
<div>
{% include_tmpl form.writing_tips %}
</div>
</li>
{% endif %}
{% for letter in form.cannedletter_set.all %}
<li>
<div class="lte-help-head">
Sample: {{ letter.subject|truncateletters:"20" }}
</div>
<div>
<h5>Subject:</h5>{{letter.subject}}
<h5>Message:</h5>{{letter.letter_text|linebreaks}}
</div>
</li>
{% endfor %}
</ul>
</script>
</div>
<div class="ak-grid-col ak-grid-col-6-of-12">
{% include "./progress_meter.html" %}
<script type="text/ak-template" for="user_info_prompt">
[% if (incomplete) { %]
<p>Please enter your information so we can find newspapers for you to contact.</p>
[% } %]
</script>
<div class="ak-styled-fields {{templateset.custom_fields.field_labels_class|default:"ak-labels-overlaid"}} {{templateset.custom_fields.field_errors_class|default:"ak-errs-below"}}">
{% include "./user_form_wrapper.html" %}
</div>
<div id="media_target"></div>
<script type="text/ak-template" for="media_target">
[% if (!incomplete) { %]
<p>Choose a newspaper to send a letter to:</p>
[%
var headers = {
"local": "Local Newspapers",
"regional": "Regional Newspapers",
"national": "National Newspapers"
};
var mediaTargets = actionkit.context.mediaTargets || {};
var mediaTargetTypes = ['national', 'regional', 'local'];
for (var j = 0; j < mediaTargetTypes.length; j++) {
var mediaTargetType = mediaTargetTypes[j];
var targetsOfType = mediaTargets[mediaTargetType];
if (targetsOfType) {
%]
<div class="ak-newspaper">
<h3>[%=headers[mediaTargetType]%]</h3>
</div>
[%
var shade = true;
for (var i = 0; i < targetsOfType.length; i++) {
var mediaTarget = targetsOfType[i],
targetId = "media_target_" + mediaTarget.id,
name = abbreviate(mediaTarget.name, 30),
label = "<a>" + name + "</a>";
if (mediaTarget.website_url) {
label = "<a target=\"_blank\" href=\"" + mediaTarget.website_url + "\">" + name + "</a>";
}
shade = !shade;
%]
<div class="[%= shade ? "shaded" : "" %] ak-newspaper-row">
<div class="ak-newspaper-title">[%=label%]</div>
<div>
<label for='[%=targetId%]'>
<input class='media_target' id='[%=targetId%]' value='[%=mediaTarget.id%]'
type='radio' name='media_target' onclick='javascript:toggleChooser(false)'>
Select</label>
</div>
<div class="number"><strong>Circulation:</strong> [%=commify(mediaTarget.circulation) %]</div>
[% if (actionkit.context.show_phones && mediaTarget.phone) { %]
<div class="nowrap"><strong>Phone:</strong> [%=mediaTarget.phone%]</div>
[% } %]
<div class="number"><strong>Sent:</strong> [%=mediaTarget.sent%]</div>
</div>
[%
}
}
}
} %]
</script>
<div id="lte-letter"></div>
<script type="text/ak-template" for="lte-letter">
[% if (!incomplete) { %]
<table class="ak-styled-fields">
<tr id="to_target_row" style="display: none;">
<td>To:</td>
<td>
<span id="to_target_name"></span>
<span style="font-size: smaller"> <a href="#" onclick="javascript:toggleChooser(true)">change</a></span>
</td>
</tr>
<tr>
<td>Subject</td>
<td><input id="letter_subject" type="text" name="subject" size="40"></td>
</tr>
<tr>
<td class="textarealabel">Message</td>
<td>
<textarea id="letter_text" name="letter_text" class="count[250]"></textarea>
<div class="wordCount"><strong>0</strong> Words. Most newspapers only consider letters of 250 to 350 words.</div>
</td>
</tr>
<tr>
<td> </td>
<td>Your name, address and phone number will be added as a signature.</td>
</tr>
</table>
[% } %]
</script>
<div id="lte-submit"><button type="submit" class="ak-styled-submit-button">Submit</button></div>
</div>
</div>
</form>
{% endblock %}
Implementation¶
There's a large amount of code that makes contexts work, and it can be tough to follow the process because it jumps back and forth between client and server side. This section outlines the basic steps you need to take to use contexts in your pages.
Main Files And Functions Involved In Contexts¶
- samples/actionkit.js
- This file adds the actionkit JS object to the window object, and has a number of methods and attributes under the headings forms and utils. The methods come together to make a fairly linear chain of calls and related callback methods, where the call causes a <script> element to be created, and the callback is what's executed as the src of that element, wrapping the JSON that the original call requested.
- samples/prefill.js
- This holds the jquery-fu that gets used to prefill whatever form is currently being acted on, if want_prefill_data and prefill are present in whatever JSON a particular callback is operating on. Prefilling usually happens from query string args after a user enters invalid data that’s only caught by the server. There’s also prefilling from data returned by the server, in the events tool.
- core.views.context()
- context() returns user info and other data in the callback( JSON ) syntax, so that it gets executed on the client side.
- core.views.text()
- Returns error messages in the user’s language, in the same JavaScript format as context()’s response.
- core.views.progress()
- Returns thermometer/progress-against-our-goal data, if none was cached and returned by context().
- your_template.html
- The template file is where you'll add javascript that uses the context data. This happens via a series of blank divs combined with matching ak-template script blocks, as in the above examples.
Code You'll Need To Write¶
The code that you will actually author and control will all live in your template file. Within your template(s), interactions with context data will happen in one of two ways: via the code in actionkit.js and prefill.js, and via your own ak-template blocks, where you'll be able to execute javascript that adds content to divs throughout your template file.
Executable ak-template Blocks¶
You can put any number of ak-template blocks into your template page, and each one will allow you to set the content of a matching div element. Divs and their templates are matched by setting the id of the div and the 'for' attribute of the ak-template script tag, like this:
<div id="all_fields"></div>
<script type="text/ak-template" for="all_fields">
<p><b>Current context:</b></p>
[%=JSON.stringify(actionkit.context)%]
</script>
What appears inside the script tag will be processed into the innerHTML of the div. You can put raw HTML inside the script tag, as well as executable javascript. The javascript must be wrapped in a special bracketing syntax to get picked up by actionkit.utils.template():
<div id="sector_x"></div>
<script type="text/ak-template" for="sector_x">
[% if (actionkit.context.show_x) { %]
<h1>[%=actionkit.context.secret_stuff%]</h1>
[% } %]
</script>
Executable blocks will just get the [% … %] wrapping, while substitution tags will get [%=var_foo%]. You can use anything that's available to you in the context, but make sure you've planned for each variable you use from the django side by placing it into the context dict in your Processor class's context() method.
Flow Of Execution For A Context¶
The execution of contexts is a collaboration between the client side Javascript code, and the methods inside the ActionKit codebase. Below is a reference for the flow of execution for a context usage, primarily focused on the Javascript methods and processing side of the equation.
SERVER: |
|
---|---|
CLIENT: |
|
SERVER: |
|
CLIENT: |
|
Function Reference¶
Below is a guide to the various javascript object methods and attributes that are available via the context system.
Attributes And Shortcut Functions¶
actionkit.context: | |
---|---|
Context from server | |
actionkit.form: | The action form (DOM element, not jQuery object) |
actionkit.forms.text: | |
Error messages (in user’s language) | |
actionkit.args: | Query string args |
$log: | Log to console (doesn’t crash on IE) |
$sel: | Search like jQuery’s $(), but limited to current form if there is more than one |
actionkit.forms¶
Attributes¶
contextRoot: | static value: '/context/' |
---|---|
dateFormat: | 'mm/dd/yy' |
dateRegexp: | /^[01]?d/[0-3]?d/dddd$/ |
timeRegexp: | /^[01]?d(:[0-5]d)?$/ |
defaultValidators: | |
everything in validators |
Methods¶
errorMessage: |
|
---|---|
fieldName: | |
beforeContextLoad: | |
|
|
loadContext: |
|
loadPrefiller: |
|
loadProgress: |
|
onProgressLoaded: | |
|
|
onPrefillerLoaded: | |
|
|
prefill: |
|
loadText: |
|
onTextLoaded: |
|
createScriptElement: | |
|
|
loadJSON: |
|
handleQueryStringErrors: | |
|
|
onContextLoaded: | |
|
|
doTemplate: |
|
onTargets: |
|
eventSearch: |
|
onEventSearchResults: | |
|
|
logOut: |
|
required: | |
validate: | |
clearErrors: | |
timeout: | |
onTimeout: | |
initPage: |
|
tryToValidate: | |
formData: | |
setForm: |
|
initForm: |
|
findConfirmationBox: | |
initValidation: | |
initTafForm: |
Validators¶
- taf_emails
- zip
- postal
- phone
- mobile_phone
- home_phone
- work_phone
- emergency_phone
- phone
- date
- time
actionkit.utils¶
Methods¶
escapeForQueryString: | |
---|---|
|
|
makeQueryString: | |
|
|
getArgs: |
|
div: |
|
makeHiddenInput: | |
|
|
appendHiddenInput: | |
|
|
makeSet: |
|
getAttr: |
|
hasAnyProperties: | |
|
|
list: |
|
val: |
|
compile: |
|
capitalize: |
|
add_commas: |
|
format: | |
template: |
|