Skip to content

Javascript Ajax Events

Djedj edited this page Feb 18, 2021 · 25 revisions

Javascript – AJAX – Events

DRAFT

The Askia Rendering Engine (Design.exe’s screen mode or AskiaExt.dll) is able to answer on certain AJAX requests.

It’s useful for live routing or to navigate in the interview without to refresh the entire page.

But the Askia Rendering Engine is not responsible about how the client browser will interpret the response of the AJAX requests.

In order to standardize the client side behaviour across ADX components, the following conventions are provided.

  1. AJAX
  2. Events
  3. Code snippets

↑ Top of page ↑

AJAX

Description

Most the AJAX requests in the Askia Rendering Engine usually post the current form in order to have the last user inputs.

Use the Askia form input name="Action" to indicates the action to execute in the server-side.

Below the list of possible actions:

Action Description
StartSurvey This is the first request action that will create an new interview.
Please avoid it’s usage in the request you create yourself.
SetIdentity This is the second request action that is automatically done to indicates the browser identity/context.
This action should be used in conjunction with the askiaidentity, askiaresolution, askiacapabilities, askiatime parameters
Please avoid it’s usage in the request you create yourself.
DoInterview This action is done to go back to an interview after disconnection or to get the last visited page.
NextPage Regular action to navigate between the next and the previous page.
Use it in conjunction with parameters such as: Next, Previous, Pause or askiaStay
DoLiveRouting Action to execute all live routing (During) and return the list of actions to execute in the client browser.
Validate (DRAFT) Action to validate the current page and return the list of possible input errors
GoTo Action to go to a specified questionnaire section, this action should be used with the shortcut parameter.
getChapters Legacy action to get the list of chapters. (using the legacy webprod scripts)
Please avoid it’s usage, the response will be a webprod script call.
EvaluateRouting Legacy action to evaluate routing (using the legacy webprod scripts)
Please avoid it’s usage, the response will be a webprod script call.

JsonFormat=bySection

For the actions such as DoInterview, NextPage and GoTo, the parameter JsonFormat with the bySection value
will produce the HTTP response with the content type application/json and the following format:


{
    "success" : bool,
    "data"	: {
        "head" : string,
        "form" : string,
        "foot" : string
    }
}

The success is a boolean value that indicates if the request was successfully executed or produce an error.

The data is an object that contains the HTML string for each ADP sections:
head: Represent the HTML string that should replace the <askia-head /> tag
form: Represent the HTML string that should replace the <askia-form>…</askia-form> tags
foot: Represent the HTML string that should replace the <askia-foot /> tag

This method is useful to navigate in the interview using AJAX requests.

↑ Top of page ↑

Action=DoLiveRouting

This action require to post the entire form (excepted binary data) to the server side.
This action will produce an HTTP response with the content type application/json and the following format:


{
    "success" : bool,
    "actions" : [
        // list of actions (see below)...
    ]
}

The success is a boolean value that indicates if the request was successfully executed or produce an error.

The actions is an array of actions that require a client side management.

Below the list of possible action format:

↑ Top of page ↑

Show / Hide

The show/hide actions are produced by the following routing actions:

Do not ask, Only ask if,
Go to, Go back to, Go without saving, Go to and mark as incomplete,
Set value and hide,
Ignore responses, Make question invisible

Show / hide a question


{
    "action"   : "showQuestion", // or "hideQuestion"
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    }
}

Show responses


{
    "action"   : "showResponses",
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    },
    "order" : [
        {
            "id"         : number,
            "inputCode"  : number,
            "entryCode"  : string
        }
    ]
}

Remarks:

- The inputCode of question is the code that suffix the input name (eg. 0 in the U0 or M0, L0…)
- The inputCode of responses is the value or the suffix that identify a question (eg. 1 in value=“1”, M0_1, R0_1 …)

↑ Top of page ↑

Reload

The current page should be reloaded or redirected.
The reload action is produced by the following routing actions:

Set language, Set charset, Set current version, Evaluate screen groups, Select next conjoint card


{
    "action" : "reload"
}

↑ Top of page ↑

Set value

The set value will strictly list the entire list of responses that should be set, all other responses should be unset.
The set value action is produced by the following routing actions:

Set value, Set value and hide

For closed questions


{
    "action" : "setValue",
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    },
    value : [
        {
            "id"         : number,
            "inputCode"  : number,
            "entryCode"  : string,
            "value"      : number // Optional: Mostly used for ranking
        }
    ]
}

For date/time questions


{
    "action" : "setValue",
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    },
    value : {
        "date"  : string, // Optional(1)
        "value" : string  // Optional(2)
    }
}

(1) The date format used is in the form yyyy-MM-dd (eg. 2016-09-28)
(2) The time format used is in the form HH:mm:ss (eg. 13:29:58)

For numeric questions


{
    "action" : "setValue",
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    },
    value : number
}

For open-ended questions


{
    "action" : "setValue",
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    },
    value : string
}

Remark:

- The value is a polymorphic key it could be a string, a number, an object or an array of responses
- The Set value and hide produce 2 actions: setValue and hideQuestion
- The inputCode of question is the code that suffix the input name (eg. 0 in the U0 or M0, L0…)
- The inputCode of responses is the value or the suffix that identify a question (eg. 1 in value=“1”, M0_1, R0_1 …)

↑ Top of page ↑

Show message

The show message action are produced by the following routing actions:

Show message

Show message


{
    "action"   : "showMessage",
    "message"  : string
}

Remarks:

- There is no difference between a blocking or a not blocking message.

↑ Top of page ↑

Change elements order

The change elements order actions are produced by the following routing actions:

Change the response order, Change order

Change the questions order


{
    "action" : "changeQuestionsOrder",
    "order"  : [
        {
            "id"        : number,
            "inputCode" : number,
            "shortcut"  : string
        }
    ]
}

Change the responses order


{
    "action"   : "changeResponsesOrder",
    "question" : {
        "id"        : number,
        "inputCode" : number,
        "shortcut"  : string
    },
    "order"   : [
        {
            "id"         : number,
            "inputCode"  : number,
            "entryCode"  : string
        }
    ]
}

Remark:

- The order will contains the full list of responses in the correct order
- The order will only contains the consecutive list of questions that changes
- The inputCode of question is the code that suffix the input name (eg. 0 in the U0 or M0, L0…)
- The inputCode of responses is the value or the suffix that identify a question (eg. 1 in value=“1”, M0_1, R0_1 …)

↑ Top of page ↑

Update caption

The ‘update caption’ action are produced by the `Live caption` propertiy on page label.

Update caption


{
    "action"   : "updateCaption",
    "selector" : string, 
    "caption"  : string
}

Remarks:

-The selector is a CSS Class (prefix `.`) or and HTML identifier (prefix `#`).

↑ Top of page ↑

Information

The information actions are produced by following routing actions:

Send e-mail, Start SQL Query, Start program, Start predictive dialing trigger,
Start recording, Interrupt recording


{
    "action"  : "info",
    "routing" : string, // See the list of possible routing above
    "state"   : string, // "success" or "error"
    "message" : string, // Optional message description
}

Possible routing

- “sendEmail”
- “startSQLQuery”
- “startProgram”
- “startPredictive”
- “startRecording”
- “stopRecording”

Remarks:

- For security reason, message is produce in rare case and generic

↑ Top of page ↑

Action=Validate

DRAFT

TODO::COMPLETE THAT PART HERE

Ideas:

- Having the possibility to validate the entire form
- Having the possibility to validate specific questions (for loop)

↑ Top of page ↑

Events

This section describe a recommended list of client side events to use (trigger and listen) on ADX projects,
in order to manage the live routing.

IMPORTANT NONE of the events described below are hard-coded. It’s all managed by ADX projects.
If there is no implementation, live routing could possibly not be triggered in the server side or could possibly not generate client side action.

Conventions

Event name

To avoid a clash with a possible future standard events or library events, all the recommended event name are prefix by askia.

Event details

For live routing the event.details is the action object that has triggered the event

Example:


// Example of function that could manage the response of the live routing AJAX query
function manageLiveRouting(jsonResponse) {
    var actions = jsonResponses.actions || [];
    for (var i = 0, l = actions.length; i < l; i += 1) {
        var item = actions[i];
        if (item.action === 'showQuestion') {
            var showQuestionEvent = new CustomEvent("askiaShowQuestion", {
                details : item
            });
            document.dispatchEvent(showQuestionEvent);
        }
    }
}

DOMElement behind events

All events are available on the document element itself.

Examples:


// Listen an event
document.addEventListener("askiaShowQuestion", function (e) {

});

// Trigger and event
var showQuestionEvent = new CustomEvent("askiaShowQuestion", {
    detail: : { id : 2, inputCode : 1, shortcut : "Q2"}
});
document.dispatchEvent(showQuestionEvent);

Remarks:

ADC may received events which doesn’t concern him, action have to filter accordingly
and the usage of the stopPropagation, stopImmediatePropagation or preventDefault must be avoid in this case.

↑ Top of page ↑

askiaAnswer

  • When: A respondent is answering a question.
  • Trigger by: ADC component each time an input change.
  • Listen by: ADP component.
  • Default action: The ADP should send an AJAX query Action=DoLiveRouting if required.

Important: The action behind the askiaSetValue event must avoid to trigger the askiaAnswer

↑ Top of page ↑

askiaShowQuestion

  • When: A question should be shown by live routing.
  • Trigger by: ADP component for each showQuestion action of the live routing response.
  • Listen by: Doesn’t necessary listen.
  • Default action: The ADP should reset the style display="" on all elements with the class name askia-question-XXX (where XXX is the question inputCode)

↑ Top of page ↑

askiaHideQuestion

  • When: A question should be hidden by live routing.
  • Trigger by: ADP component for each hideQuestion action of the live routing response.
  • Listen by: Doesn’t necessary listen.
  • Default action: The ADP should set the style display="none" on all elements with the class name askia-question-XXX (where XXX is the question inputCode)

↑ Top of page ↑

askiaShowResponses

  • When: Responses of a question should be shown by live routing.
  • Trigger by: ADP component for each showResponses action of the live routing response.
  • Listen by: ADC component.
  • Default action: None. Each ADC components should manage this event accordingly

↑ Top of page ↑

askiaHideResponses

  • When: Responses of a question should be hidden by live routing.
  • Trigger by: ADP component for each hideResponses action of the live routing response.
  • Listen by: ADC component.
  • Default action: None. Each ADC components should manage this event accordingly

↑ Top of page ↑

askiaReload

  • When: Reload action is require by live routing.
  • Trigger by: ADP component for each reload action of the live routing response.
  • Listen by: Doesn’t necessary listen.
  • Default action: The ADP should reload the current page.

↑ Top of page ↑

askiaSetValue

  • When: Value(s) of a question should be set by live routing.
  • Trigger by: ADP component for each setValue action of the live routing response.
  • Listen by: ADC component.
  • Default action: None. Each ADC components should manage this event accordingly

Important: The action behind the askiaSetValue event must avoid to trigger the askiaAnswer

↑ Top of page ↑

askiaShowMessage

  • When: A message should be shown by live routing.
  • Trigger by: ADP component for each showMessage action of the live routing response.
  • Listen by: ADP component.
  • Default action: None. The ADP component should manage this event accordingly

↑ Top of page ↑

askiaChangeQuestionsOrder

  • When: The order of questions should be changed by live routing.
  • Trigger by: ADP component for each changeQuestionsOrder action of the live routing response.
  • Listen by: ADP component.
  • Default action: None. The ADP component should manage this event accordingly

↑ Top of page ↑

askiaChangeResponsesOrder

  • When: The order of responses should be changed by live routing.
  • Trigger by: ADP component for each changeResponsesOrder action of the live routing response.
  • Listen by: ADC component.
  • Default action: None. The ADC component should manage this event accordingly

↑ Top of page ↑

askiaUpdateCaption

  • When: Whenever a refresh is done some captions might change (their class is askia-liveXX)
  • Trigger by: any change to the question.
  • Listen by: ADP component
  • Default action: The ADP component should manage this event accordingly

↑ Top of page ↑

askiaInfo

  • When: An information is provided by live routing.
  • Trigger by: ADP component for each info action of the live routing response.
  • Listen by: Doesn’t necessary listen.
  • Default action: None.

↑ Top of page ↑

Code snippets

Code load on the page through an ADP


// Static ADP file
(function () {

    // Augment or create the public `askia` namespace
    var askia = window.askia || {};
    if (!window.askia) {
        window.askia = askia;
    }

    /* ---======== Utilities ========--- */

    /**
     * Capitalize the first letter of the string and return the new string
     *
     * @param {String} str String to capitalize
     */
    function capitalize(str) {
        return str.charAt(0).toUpperCase() + str.slice(1);
    }

    /**
     * Iterate over all submittable elements of a form
     * )This method was inspired from jQuery.serializeArray)
     *
     * @param {HTMLElement} elForm Form element to parse
     * @param {Function} fn Function called for each submittable elements
     * @param {HTMLElement} fn.element Submittable element
     */
    function forEachSubmittableElements(elForm, fn) {
        if (typeof fn !== 'function') {
          return;
        }

            // Don't submit all input submittable
        var rgSubmitter = /^(?:submit|button|image|reset|file)$/i,
            // Submittable elements
            rgSubmittable = /^(?:input|select|textarea|keygen)/i,
            // Elements that have a checked state
            rgCheckable = /^(?:checkbox|radio)$/i,
            // List of elements
            els    = elForm.elements,
            i, l;

        for (i = 0, l = els.length; i < l; i += 1) {
            var el = els[i];
            if ( !el.name || el.disabled || el.value === null ||
                    rgSubmitter.test(el.type) ||
                    !rgSubmittable.test(el.nodeName) ||
                    (rgCheckable.test(el.type) && !el.checked)) {

                continue;
            }

            fn(el);
        }
    }

    /**
     * Serialize the Askia Form to an object
     *
     * @param {String} [action] Action to use instead of the regular form action
     * @return {String} Return the form data that should normally be send to the server-side
     */
    askia.serializeForm = function serializeForm(action) {
        var params = [];
        forEachSubmittableElements(function (el) {
            var name  = el.name
            var value = el.value.replace(/\r?\n/gi, "\r\n");
            if (action && /^(?:action)$/i.test(el.name)) {
                value = action.replace(/\r?\n/gi, "\r\n");
            }
            params.push(encodeURIComponent(name) + "=" + encodeURIComponent(value));
        });
        return params.join('&');
    };

    /**
     * Execute an AJAX query
     *
     * @param {Object} query AJAX query to execute
     * @param {String} url URL of the server-side management
     * @param {"GET"|"POST"|string} [method="POST"] Request method to use
     * @param {String} [data=null] Data to send to the server side
     * @param {Function} [success] Callback on success
     * @param {String} success.text Text of the response
     * @param {XMLHttpRequest} success.xhr XMLHTTPRequest used
     * @param {Function} [error] Callback on error
     * @param {String} error.text Text of the response
     * @param {XMLHttpRequest} error.xhr XMLHTTPRequest used
     * @param {Function} [complete] Callback on query complete (success or error)
     * @param {String} complete.text Text of the response
     * @param {XMLHttpRequest} complete.xhr XMLHTTPRequest used
     */
    askia.ajax = function ajax(query) {
        if (!query) {
            (console && console.warn("The `query` argument must be a valid object for askia.ajax()"));
            return;
        }
        if (!query.url || typeof query.url !== 'string') {
            (console && console.warn("The `query.url` argument must be a valid string for askia.ajax()"));
            return;
        }

        query.method = ((query.method && query.method.toString()) || "POST").toUpperCase();
        if (/^(?:GET|POST|PUT|DELETE|HEAD|OPTIONS|TRACE|CONNECT)$/.test(query.method)) {
            (console && console.warn("The `query.method` argument must be a valid HTTP method for askia.ajax()"));
            return;
        }

        var xhr = new XMLHttpRequest();
        xhr.open(query.method, query.url, true);
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
        xhr.onload = function onXhrLoad() {
            var text = xhr.responseText;
            if (xhr.status >= 200 && xhr.status < 400) {
                if (typeof query.success === 'function') {
                    query.success(text, xhr);
                }
            } else {
                if (typeof query.error === 'function') {
                    query.error(text, xhr);
                }
            }

            if (typeof query.complete === 'function') {
                query.complete(text, xhr);
            }
        };

        request.onerror = function() {
            if (typeof query.error === 'function') {
                query.error(text, xhr);
            }
            if (typeof query.complete === 'function') {
                query.complete(text, xhr);
            }
        };

        xhr.send(query.data || null);
    };


    /* ---======== Askia Events Management ========--- */

    askia.defaultEventActions = {
        askiaAnswer                 : executeLiveRouting,
        askiaShowQuestion           : executeShowHideQuestion,
        askiaHideQuestion           : executeShowHideQuestion,
        askiaShowResponses          : null,
        askiaHideResponses          : null,
        askiaPost                   : executePost,
        askiaSetValue               : null,
        askiaShowMessage            : null,
        askiaChangeQuestionsOrder   : null,
        askiaChangeResponsesOrder   : null,
        askiaInfo                   : null
    };


    /**
     * Trigger an arbitrary event
     *
     * @param {String} eventName Name of the event to trigger
     * @param {Object} details Details associated with the event
     */
    askia.triggerEvent = function triggerEvent (eventName, details) {
        var eventInit = details !== undefined ? { details : details } : undefined;
        var event = new CustomEvent(eventName, eventInit);
        return document.dispatchEvent(event);
    };

    /**
     * Trigger an event when the respondent is answering
     */
    askia.triggerAnswer = function triggerAnswer() {
        if (!askia.triggerEvent("askiaAnswer")) {
            return false; // preventDefault() has been called
        }

        // Default behaviour
        askia.defaultEventActions.askiaAnswer();
    };

    /* ---======== Default Events Management ========--- */

    /**
     * Show or hide an entire question
     *
     * @param {Object} data Definition of the action to do
     * @param {"showQuestion"|"hideQuestion"} data.action Action to execute
     * @param {Number} data.inputCode Input code associated with the question
     */
    function executeShowHideQuestion(data) {
        if (!data.hasOwnProperty('inputCode')) {
            return;
        }

        var isShow    = /^(?:show)/i.test(data.action),
            className = '.askia-question-' + data.inputCode;
            elements  = document.querySelectorAll(className),
            i, l;
        for (i = 0, l = elements.length; i < l; i += 1) {
            elements[i].style.display = isShow ? '' : 'none';
        }
    }

    /**
     * Post the form
     */
    function executePost() {
        document.forms[0].submit();
    }

    /* ---======== Live Routing Management ========--- */

    var isExecutingLiveRouting     = false,     // Flag to avoid several live routing request
        shouldReExecuteLiveRouting = false;     // Flag to re-execute the live routing

    /**
     * Execute the AJAX query to do a live routing
     */
    function executeLiveRouting() {
        if (isExecutingLiveRouting) {
            shouldReExecuteLiveRouting = true;
            return;
        }
        isExecutingLiveRouting = true;
        shouldReExecuteLiveRouting = false;
        askia.ajax({
            url      : 'AskiaExt.dll',
            data     : askia.serializeForm(document.forms[0], "DoLiveRouting"),
            success  : onLiveRoutingSuccess,
            complete : onLiveRoutingComplete
        });
    }

    /**
     * Manage the live routing AJAX - success
     */
    function onLiveRoutingSuccess(text) {
        var json = JSON.parse(text);
        var actions = json.actions || [];
        var i, l, itemAction, eventName;
        for (i = 0, l = actions.length; i < l; i += 1) {
            itemAction = actions[i];
            eventName = "askia" + capitalize(itemAction.action);
            if (!askia.triggerEvent(eventName, itemAction)) {
                continue; // preventDefault();
            }
            // Default behaviour
            if (typeof askia.defaultEventActions[eventName] === 'function') {
                askia.defaultEventActions[eventName](itemAction);
            }
        }
    }

    /**
     * Manage the live routing AJAX - complete
     */
    function onLiveRoutingComplete() {
        isExecutingLiveRouting = false;
        if (!shouldReExecuteLiveRouting) {
            return;
        }
        setTimeout(executeLiveRouting, 250);
    }

}());

Examples of usage


// Display a routing message inside an HTMLElement
document.addEventListener("askiaShowMessage", function (data) {
    document.getElementById("messageContainer").innerHTML = data.message;
});

// An ADC, Notify that the user give a response
document.getElementById("myOpenText").addEventListener("input", function () {
    if (window.askia && typeof window.askia.triggerAnswer === 'function') {
        window.askia.triggerAnswer();
    }
});

↑ Top of page ↑

<< Unit tests