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"> &nbsp; <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:
  • Request is made to apache; Template file is found, served with wrapper.
  • Subsequent request comes in for /samples/actionkit.js.
CLIENT:
  • actionkit.js:
    • Adds the actionkit object to the window object.
    • Adds utils and forms to window.actionkit.
    • Adds a large number of methods and attributes to utils and forms.
  • wrapper.html (line 60) calls actionkit.forms.initForm('act'):
    • forms.initForm() (line 748 of actionkit.js):
      • forms.setForm() (line 703) sets the form element that utils and forms methods will work on.
      • Sets the form's onsubmit to a function that will run forms.tryToValidate()
      • Runs forms.loadPrefiller() if prefill and want_prefill_data are in the args. That loads /samples/prefill.js into the document head with forms.createScriptElement().
      • Calls forms.loadContext(), which:
        • Calls forms.beforeContextLoad(), which checks for some event related args.
        • Sets forms.onContextLoaded as the callback function that will be used to process what is returned from the server as JSON.
        • Sets a number of contextArgs values from ak.args, ak: form_name, action_id, akid, rd, want_progress, template, want_prefill_data.
        • Sets contextArgs.required to forms.required(), which returns a list of required names based on what's in the 'required' element in the form.
        • Creates a contextUrl from the contextRoot and the contextArgs, and uses forms.createScriptElement to load that contextUrl as the src of a script element in the document head.
SERVER:
  • Request comes in from the browser for the constructed contextUrl, which will be of the type /context/?a=b&c=d...
  • ActionKit code does the following (paraphrasing here--the contexts interface is stable, but AK internals may change):
    • Finds the specific page object that the contextUrl referenced.
    • Creates a blank dictionary (python's associative array datatype) to store context info in.
    • Checks for required fields in the submission, and for this page.
    • Checks for language id and custom field info for this page.
    • Checks for any page-type specific fields, like media targets for LTEs.
    • Adds all that info to the context dictionary.
    • Renders that context dictionary as a JSON string.
    • Returns the JSON, wrapped by the callback function specified, as the executable src of a <script> tag.
CLIENT:
  • The text/javascript content returned by the server is run, as it's the src of the <script> element that got created via forms.createScriptElement().
  • Since the generic callback for forms.loadContext() is forms.onContextLoaded(), that runs with the JSON as its argument.
  • forms.onContextLoaded(), line 242 in actionkit.js:
    • If ak.context is already set, returns nothing; otherwise sets ak.context to the JSON argument it got.
    • Finds out whether it can recognize the user based on the info it has.
    • If it can, puts the akid into a hidden input and hides the user form.
    • If it can't, shows the unknown user form.
    • Appends several fields as hidden inputs, if they're present in the context.
    • Runs forms.onTargets() if there are targets.
    • Sets context.args to ak.args and adds some methods to context.
    • Runs forms.loadProgress() if necessary.
    • Selects all templates in the page (script tags with the type 'text/ak-template'), and runs forms.doTemplate(context, template) on each.
    • forms.doTemplate() does the following:
      • Runs the context and template through utils.template, which:
        • Generates a cached function based on the javascript in the ak-template
        • Places the output of that function into the innerHTML of the div that has the id which the ak-template section's for attribute lists.
    • Prefills the form if possible.
    • Runs forms.loadText(), which will perform internationalization on some of the context's strings if necessary/possible.
    • Runs forms.handleQueryStringErrors(), which packs up errors and messages into ak.errors, and then runs forms.onValidationErrors()
    • forms.onValidationErrors() does:
      • Clears existing errors from the form.
      • Marks the controls which have errors.
      • Marks the labels to those controls.
      • Marks the form with the class name contains-errors

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:
  • Takes an error name like 'card_num:invalid'
  • Returns a capitalized error string
fieldName:
beforeContextLoad:
 
  • Events Only
loadContext:
  • Runs beforeContextLoad
  • Adds action_id, akid, rd, want_progress, template, want_prefill_data, url to contextArgs
  • Uses a random number to keep ak from caching the response
  • Creates a script element from contextUrl
loadPrefiller:
  • pulls in the prefill script, /samples/prefill.js, into a script element
loadProgress:
  • pulls in /progress?page= page id & form_name & callback = onProgressLoaded
onProgressLoaded:
 
  • creates a script element that's an ak-template for 'progress'
onPrefillerLoaded:
 
  • runs forms.prefill() if ak.forms.awaitingPrefill
prefill:
  • If there's a context and prefill data, use the prefill data, else use the args
  • If there's a form, use setForm
  • Use $().deserialize() to set the form values
  • Use a jquery each() loop to set checkbox values
loadText:
  • brings in translated errors from /text/ using the same script-element technique as loadContext()
onTextLoaded:
  • sets forms.text to the input, is the callback for loadText
createScriptElement:
 
  • creates a <script> element consisting of an src that is the url passed in, and any additional attributes
loadJSON:
  • maintains a callback_id counter and a var 'actionkitCallback'+callback_id
  • registers a callback with the window obj
  • adds the callback to args via args.callback = 'window.'+callback_name
  • creates a script element via a url+args
handleQueryStringErrors:
 
  • if there's no form in ak.args.form_name, do nothing
  • for each key in actionkit.args like ^(error|message)_, put them into ak.errors
  • if utils.hasAnyProperties( the errors ): ak.forms.onValidationErrors( the errors )
onContextLoaded:
 
  • is the callback for forms.loadContext()
  • takes a context as an arg
  • if ak.context is already set, returns nothing
  • otherwise sets ak.context to the arg
  • finds out whether it can recognize the person based on the info it has
  • if the person is recognized, stick the akid into a hidden input and hide the user form
  • if they're not, show the unknown user form
  • append several fields as hidden inputs if they exist
  • run forms.onTargets() if there are targets
  • sets context.args to ak.args, adds some functions to context
  • runs loadProgress if necessary
  • selects all the templates in the page
  • for each, runs forms.doTemplate(context, template)
  • prefills if it can
  • runs forms.loadText()
  • runs forms.handleQueryStringErrors()
doTemplate:
  • runs the supplied context and element through utils.template()
  • makes the output the innerHTML of the element
onTargets:
  • does some pluralization, adds checkbox and listing html to 'target_checkboxes' element
eventSearch:
  • events only
onEventSearchResults:
 
  • events only
logOut:
  • if there's an akid, store it as referring_akid
  • if there's no akid, log the person out and redirect to their next location
  • otherwise, just strip the akid and rework the query string without it
required:
validate:
clearErrors:
timeout:
onTimeout:
initPage:
  • adds 'js' class to the document body
  • clears some window caching
  • loads firebug lite in IE if debug is on
tryToValidate:
formData:
setForm:
  • sets the current form that ak.form is acting on
initForm:
  • takes a form name
  • sets the form via setForm
  • if ak.form.onsubmit isn't set, sets it to a function that runs tryToValidate
  • if the prefill is on, run loadPrefiller()
  • run loadContext()
findConfirmationBox:
 
initValidation:
initTafForm:

Validators

  • email
  • taf_emails
  • zip
  • postal
  • phone
  • mobile_phone
  • home_phone
  • work_phone
  • emergency_phone
  • phone
  • date
  • time

actionkit.utils

Methods

escapeForQueryString:
 
  • URI encodes a string and returns it
makeQueryString:
 
  • turn an object into an escaped query string
getArgs:
  • takes the url args and returns them as an object
div:
  • creates a div
makeHiddenInput:
 
  • adds a hidden input tag to a div as the first child
  • is called by appendHiddenInput
appendHiddenInput:
 
  • sticks a hidden input with name/value in the current form
makeSet:
  • turns a list into a dict/hash
getAttr:
  • returns the value of an attribute for a given element
  • used in onProgressLoaded, handleQueryStringErrors, doTemplate, validate, setForm
hasAnyProperties:
 
  • runs through a list of properties, returns true if any are present
list:
  • returns a list
  • used in forms.required
val:
  • returns the value of an element
  • used in validators.zip, validators.phone, forms.validate
compile:
  • pass a function name and a parameter list, it'll return the eval'd version of same
  • used in forms.validate
capitalize:
  • uppercases and the first letter of a string passed to it
  • used in forms.errorMessage, forms.onContextLoaded
add_commas:
  • commifys a number
  • used in forms.onContextLoaded
format:
template:
  • takes a string and data
  • looks for the result in a cache
  • otherwise processes the result through a generic function
  • which returns a js-safe version of the string