-
Notifications
You must be signed in to change notification settings - Fork 3
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.
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 parametersPlease 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. |
|
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. |
|
Legacy action to evaluate routing (using the legacy webprod scripts) Please avoid it’s usage, the response will be a webprod script call. |
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.
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:
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 …)
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"
}
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 …)
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.
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 …)
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 `#`).
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
DRAFT
TODO::COMPLETE THAT PART HERE
Ideas:
- Having the possibility to validate the entire form
- Having the possibility to validate specific questions (for loop)
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.
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.
- 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
- 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 nameaskia-question-XXX
(where XXX is the questioninputCode
)
- 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 nameaskia-question-XXX
(where XXX is the questioninputCode
)
- 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
- 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
- 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.
- 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
- 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
- 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
- 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
- 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
- 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.
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();
}
});