surveyJS let's you create a survey from a JSON and manage all the process ( fields validation, local storage and JSON construction are already set )
You can manage:
surveyJS is compatible with ( desktop & mobile versions ):
* for compatibility with IE 10/11 and old versions of Edge you have to load some JS polyfills ( from polyfill.io or else ):
Array.from
Element.prototype.closest
Element.prototype.matches
Event
WeakMap
<script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=Array.from,Element.prototype.closest,Element.prototype.matches,Event,WeakMap"></script>
surveyJS is developed with these dependencies:
<!-- BOOTSTRAP CSS --> <link rel="stylesheet" href="css/bootstrap.css"> <!-- SURVEY CSS --> <link rel="stylesheet" href="css/survey.css"> <!-- JQUERY --> <script src="js/vendors/jquery-1.12.4.min.js"></script> <!-- SURVEY JS ( with WEB_STORAGE and formJS modules included ) --> <script src="js/surveyJS.min.js"></script>
This is the basic HTML structure to initialize the survey ( div with class "surveyjs-body" will be filled with questions & answers. ):
<div class="surveyjs-container" data-surveyjs-container> <form action="page.jsp" name="surveyjs-form" class="surveyjs-form" data-surveyjs-form novalidate> <div class="surveyjs-body questionsList clearfix" data-surveyjs-body></div> <div class="surveyjs-footer"> <button type="submit">SEND</button> </div> </form> </div>
Mandatory attributes:
data-surveyjs-*
attributes
action
the page where to send the users answers ( with the AJAX call )
novalidate
prevent the HTML5 default validation of required fields ( they will be validated via JS )
CSS classes ( like .surveyjs-*
) are optional but used to give styles to the demos.
For custom graphic inputs, using class surveyjs-custom-inputs
for the Survey container ( with class "surveyjs-container" ) is suggested in order to default Survey CSS be applied.
You must specify the url to retrieve the JSON data to build the survey.
SURVEY.setup({ url: 'json/survey.json' });
Example of JSON file for the survey is here.
( image, title and description are optional )
Note that the AJAX call to get the JSON file MUST return it as JSON string or JS/JSON object ( it's the same for the response from survey submit ).
Below the explanation of the JSON:
{ "result": "OK", // IF "OK" THE SURVEY WILL BE BUILT "survey": { "id": 0, "image": "img/surveyjs-image.png", "title": "My Survey", "description": "Answer the questions", // LIST OF ALL QUESTIONS ( AS OBJECTS ) "questions": [ // RADIO ANSWER { // EVERY QUESTION HAS ITS OWN DATA "id": "1", "question": "Hai animali domestici?", "answers": [ // EACH ANSWER IS AN OBJECT WITH SOME DATA { "id": "1", "type": "radio", "answer": "Sì", "sort": 1 }, { "id": "2", "type": "radio", "answer": "No", "sort": 2 } ], // IF required IS FOUND, THE ANSWER FIELDS WILL HAVE THE required ATTRIBUTE (SO THIS QUESTION REQUIRE AN ANSWER) "required": "", // THE POSITION TO SET FOR THE QUESTION (USED ALSO FOR ANSWERS - SEE ABOVE) - THIS IS OPTIONAL "sort": 1 }, // TEXT INPUT ANSWER NOT MANDATORY { "id": "1b", "question": "Hai animali domestici?", "answers": [ { "id": "1ba", "type": "text", "answer": "" } ] }, // RELATED FIELD ANSWER WITH INPUT TEXT ( SEE attribute FIELD ) { "id": "5", "question": "Quanto è importante che il programma fedeltà...?", "answers": [ { "id": "19", "type": "radio", "answer": "Sia divertente" }, { "id": "20", "type": "radio", "answer": "Non richieda molto impegno di partecipazione" }, { "id": "200", "type": "radio", "answer": "Altro...", "attribute": { "id": "", "type": "text", "answers": "" } } ], "required": "", "sort": 6 }, // NESTED ANSWERS ( SEE nested ATTRIBUTE ) // RELATED FIELD ANSWER WITH SELECT FIELD ( SEE attribute FIELD ) { "id": "6", "question": "Quale MODALITÀ DI PARTECIPAZIONE al programma fedeltà preferisci?", "answers": [ { "id": "21", "type": "radio", "answer": "Sito", "nested": [ { "id": "21a", "type": "radio", "answer": "Pc", "sort": 1 }, { "id": "21b", "type": "radio", "answer": "Smartphone", "sort": 2 }, { "id": "21c", "type": "radio", "answer": "Tablet", "sort": 3 } ], "sort": 1 }, { "id": "21aPlus", "type": "radio", "answer": "Sito", "attribute": [ { "id": "21aa", "type": "option", "answer": "Pc", "sort": 1 }, { "id": "21ba", "type": "option", "answer": "Smartphone", "sort": 2 }, { "id": "21ca", "type": "option", "answer": "Tablet", "sort": 3 } ], "sort": 6 }, { "id": "24", "type": "radio", "answer": "App", "sort": 3 }, { "id": "25", "type": "radio", "answer": "Telefonica", "sort": 4 }, { "id": "26", "type": "radio", "answer": "Con utilizzo card/tessera", "sort": 5 } ], "required": "", "sort": 7 }, // CHECKBOX ANSWER WITH MULTIPLE CHOICE ( maxChoice: NUMBER OF MAXIMUM SELECTABLE ANSWERS ) { "id": "8", "question": "Cosa hai gradito di più del programma in corso?", "answers": [ { "id": "43", "type": "checkbox", "answer": "Chiarezza della comunicazione", "sort": 1 }, { "id": "44", "type": "checkbox", "answer": "Facilità e modalità di partecipazione", "sort": 2 }, { "id": "45", "type": "checkbox", "answer": "Stile del programma (grafica e linguaggio)", "sort": 3 }, { "id": "46", "type": "checkbox", "answer": "Durata del tempo di partecipazione", "sort": 4 }, { "id": "47", "type": "checkbox", "answer": "Contenuti/premi proposti", "sort": 5 } ], "checks": "[1,2]", "required": "", "sort": 9 }, // SELECT ANSWER { "id": "9", "question": "Quanto è importante che il programma fedeltà sia divertente?", "answers": [ { "id": "160", "type": "option", "answer": "Poco", "sort": 1 }, { "id": "161", "type": "option", "answer": "Così così", "sort": 2 }, { "id": "162", "type": "option", "answer": "Tanto", "sort": 3 } ], "required": "", "sort": 10 }, // PRIVACY CHECK { "id": "22", "question": "hidden-privacy", "answers": [ { "id": "44", "type": "radio", "answer": "Sì" }, { "id": "45", "type": "radio", "answer": "No" } ], "required": "", "sort": 10 }, ] } }
Note:
To bind the privacy acceptance field to the survey you can set a question object as shown in the JSON above ( see PRIVACY CHECK ):
<div class="surveyjs-question-box" data-formjs-question> <div class="surveyjs-answers-box"> <div class="form-check-inline"> <input data-exclude-storage data-name="bind-surveyjs-answer"/> <label> <span></span> </label> </div> <div class="form-check-inline"> <input data-exclude-storage data-name="bind-surveyjs-answer"/> <label> <span></span> </label> </div> </div> </div>
You can initialize the survey with some extra options:
cssClasses
JS object with keys:
fieldErrorFeedback
whether or not to load the field error message inside the HTML ( shown on failed field validation )
fieldErrorMessage *
Text to show on generic field error validation.
See below for default messages ( depending on the language ).
fieldErrorMessageMultiChoice *
Text to show on error validation of checkboxes with multiple choice.
See below for default messages ( depending on the language ).
fieldOptions
JS object with field options passed to isValidField method of Form instance.
See formJS plugin for details.
formOptions
JS object with form options passed to the Form instance creation.
See formJS plugin for details.
lang
language for messages (eg: loading box), select default option etc...
other values: 'it'
loadingBox *
HTML container and text for the loading box (used when requesting the survey JSON file).
See below for default messages ( depending on the language ).
maxChoiceText *
This text can be used for quesions that let the user choose more than one answer (checkboxes).
See below for default messages ( depending on the language ).
onInitComplete( $surveyForm )
Fires when the AJAX call ( to get the survey JSON ) ends.
Parameters:
onInitError( jqXHR, textStatus, errorThrown, $surveyForm )
Fires when the AJAX call ( to get the survey JSON ) throws an error.
Parameters:
onInitSuccess( json, $surveyForm )
Fires when all the survey is successfully loaded in the page.
Parameters:
selectFirstOption *
Used if an option with empty id is NOT found in the JSON.
See below for default messages ( depending on the language ).
templates.fieldError
HTML string to be used as field error container ( when showing an error message on failed field validation ).
See Templates section.
templates.input
HTML string to be used for input fields when generating the survey
See Templates section.
templates.inputGroup
HTML string to be used for input group answers when generating the survey.
See Bootstrap Input Group
See Templates section.
templates.inputTag
HTML string to be used as input tags when generating the survey
See Templates section.
templates.labelTag
HTML string to be used for label tags when generating the survey
See Templates section.
templates.question
HTML string to be used as question (and answers) container when generating the survey
See Templates section.
templates.select
HTML string to be used for select answers when generating the survey
See Templates section.
templates.selectTag
HTML string to be used for select tags when generating the survey
See Templates section.
templates.textarea
HTML string to be used for textarea answers when generating the survey
See Templates section.
textareaPlaceholder *
See below for default messages ( depending on the language ).
useLocalStorage
Whether or not to use JS local storage to save user answers while compiling the survey.
When reloading the page, JS local storage will be used to polulate the questions with the answers selected/typed by the user.
Note that localStorage for the survey will be cleared on submit success.
Here an example of custom initialization:
SURVEY.setup({ url: 'json/survey.json', onInitSuccess: function( json, $surveyForm ){ var $surveyBody = $surveyForm.find('.surveyjs-body'), initStatus = json.status; if( initStatus === 'success' ){ $('.stackSlider').stackslider({ piles : false }); } else { $surveyForm.find('.surveyjs-footer').remove(); $surveyBody.html( '<div class="surveyjs-message">Errore durante il caricamento. Per favore, ricarica la pagina.</div>' ); } }, onInitError: function( jqXHR, textStatus, errorThrown, $surveyForm ){ $surveyForm.find('.surveyjs-loading').html( '<div class="surveyjs-message">Errore durante il caricamento. Per favore, ricarica la pagina.</div>' ); }, fieldOptions: { onValidation: function( fields ){ if( fields.length > 1 ){ // GO TO THE FIRST UNANSWERED QUESTION var $stWrapper = $('.surveyjs-form .st-wrapper'), activeIndex = $stWrapper.find('.st-center').index(), $invalidField = (function(){ for( var f=0; f<fields.length; f++ ){ var obj = fields[f]; if( !obj.result && !$(obj.field).is('[data-name="bind-surveyjs-answer"]') ){ return $(obj.field); } } return $(); })(); if( $invalidField.length ){ var invalidIndex = $invalidField.closest('.st-item').index(), clickDirection = ( activeIndex > invalidIndex ? 'prev' : 'next' ), $btn = ( clickDirection === 'prev' ? $stWrapper.find('nav > span:first-child') : $stWrapper.find('nav > span:last-child') ), clicksLength = ( clickDirection === 'prev' ? activeIndex - invalidIndex : invalidIndex - activeIndex ); for(var i=0; i<clicksLength; i++){ $btn.trigger('mousedown.stackslider'); $btn.trigger('mouseup.stackslider'); } } } else { console.log('validating field ', fields[0]); } } }, formOptions: { beforeSend: function( data ){ console.log('Survey formOptions.beforeSend call...', data); if( !data.stopExecution ){ //$surveyForm.find('[type="submit"]').button('loading'); $surveyCont.find('.alert').remove(); data.formData = 'surveyJSON=' + JSON.stringify( data.formData ); } return data; }, onSubmitComplete: function( ajaxData ){ console.log('formOptions.onSubmitComplete', ajaxData); //$surveyForm.find('[type="submit"]').button('reset'); }, onSubmitError: function( ajaxData ){ console.log('Survey formOptions.onSubmitError', ajaxData); // PRINT THE ERROR MESSAGE AFTER THE FORM $surveyForm.closest('.surveyjs-container').append( '<div class="alert alert-danger text-center" role="alert"><p>Generic error, please retry.</p></div>' ); }, onSubmitSuccess: function( ajaxData ){ console.log('Survey formOptions.onSubmitSuccess', ajaxData); if( typeof ajaxData === 'string' ){ try{ ajaxData = JSON.parse(ajaxData); } catch(error){ throw new Error('String returned from ajax call to "' + ajaxOptions.url + '" is not a valid JSON string'); } } if( $.isPlainObject( ajaxData ) ){ if( ajaxData.data.status === 'success' ){ // REMOVE THE SURVEY FORM FROM THE PAGE AND PRINT A RESPONSE MESSAGE $surveyForm.parent().html( '<div class="alert alert-success text-center" role="alert"><p>Great! You completed the mission.</p></div>' ); // OPEN THE BOOTSTRAP MODAL TO SHOW A CONGRATULATION MESSAGE $('#modal-notification').modal('show'); } else { // PRINT THE ERROR MESSAGE AFTER THE FORM $surveyForm.closest('.surveyjs-container').append( '<div class="alert alert-danger text-center" role="alert"><p>Generic error, please retry.</p></div>' ); } } } } });
You can add a new language ( or override an existing one ), with SURVEY.addLanguage( langString, langObject )
.
This will also set the language used for the survey.
You MUST pass all the key/value pairs as shown below:
var newLang = { fieldErrorMessage: 'Du musst antworten', fieldErrorMessageMultiChoice: 'Sie Können zwischen 1 und 3 Antworten wählen.', loadingBox: '<div class="surveyjs-loading" data-surveyjs-loading><i class="glyphicon glyphicon-refresh icon-spin"></i> Wird geladen...</div>', maxChoiceText: 'ANTWORTEN MAX', selectFirstOption: 'Wähle eine Antwort...', textareaPlaceholder:'Schreibe eine Antwort...' }; SURVEY.addLanguage( 'de', newLang );
You can initialize the survey with your custom HTML templates for:
Remember to use ALL the data-* attributes and placeholders with the curly braces, for example {{questionId}}, in your custom code.
Here some examples of initialization with custom templates ( these are, actually, the basic templates ):
Custom Template for Loading Box
SURVEY.setup({ url: 'json/survey.json', loadingBox: '<div class="surveyjs-loading" data-surveyjs-loading><i class="glyphicon glyphicon-refresh icon-spin"></i> Loading...</div>' });
Custom Template for Fields Error
SURVEY.setup({ url: 'json/survey.json', templates: { fieldError: '<div class="surveyjs-field-error-message">{{fieldErrorMessage}}</div>' } });
Custom Template for Inputs
SURVEY.setup({ url: 'json/survey.json', templates: { input: '<div class="surveyjs-single-answer surveyjs-input-container surveyjs-answer-{{answerType}} form-check" data-answer-index="{{answerIndex}}">'+ '{{inputTagCode}}'+ '{{labelTagCode}}'+ '</div>' } });
Custom Template for Input Groups
SURVEY.setup({ url: 'json/survey.json', templates: { inputGroup: '<div class="surveyjs-single-answer input-group" data-answer-index="{{answerIndex}}">'+ '<div class="input-group-prepend">'+ '<div class="input-group-text form-check surveyjs-answer-{{answerType}}">'+ '<input type="{{answerType}}" name="surveyjs-answer-{{questionNumber}}" id="{{answerCode}}" data-answer-id="{{answerId}}" value="{{answerIdValue}}" {{attrRequired}} data-require-more="" class="surveyjs-input surveyjs-radio form-check-input" />'+ '<label for="{{answerCode}}" class="surveyjs-label form-check-label">{{answerString}}</label>'+ '</div>'+ '</div>'+ '{{relatedAnswerField}}'+ '</div>' } });
Custom Template for Input Tags
SURVEY.setup({ url: 'json/survey.json', templates: { inputTag: '<input type="{{answerType}}" name="surveyjs-answer-{{questionNumber}}{{addMoreName}}" class="surveyjs-input surveyjs-{{answerType}} {{fieldClass}}" id="{{answerCode}}" {{nestedAnswer}} data-answer-root="{{progIdsJoined}}" data-answer-id="{{answerId}}" value="{{answerIdValue}}" {{attrRequired}} {{attrChecks}} {{attrRequiredFrom}} />' } });
Custom Template for Label Tags
SURVEY.setup({ url: 'json/survey.json', templates: { labelTag: '<label for="{{answerCode}}" class="surveyjs-label {{labelClass}}">{{answerString}}</label>' } });
Custom Template for Questions
SURVEY.setup({ url: 'json/survey.json', templates: { question: '<div data-question-id="{{questionId}}" data-question-index="{{questionNumber}}" data-formjs-question class="surveyjs-question-box clearfix">'+ '<div class="surveyjs-question-header">Question {{questionNumber}}</div>'+ '<div class="surveyjs-question-body">'+ '<div class="surveyjs-question-text">{{questionText}}</div>'+ '<div class="surveyjs-answers-box form-group clearfix">'+ '{{answersHtml}}'+ '{{fieldErrorTemplate}}'+ '</div>'+ '</div>'+ '</div>' } });
Custom Template for Selects
SURVEY.setup({ url: 'json/survey.json', templates: { select: '<div class="surveyjs-single-answer surveyjs-answer-select" data-answer-index="{{answerIndex}}">'+ '{{selectTagCode}}'+ '</div>' } });
Custom Template for Select Tags
SURVEY.setup({ url: 'json/survey.json', templates: { selectTag: '<select id="{{answerCode}}" name="surveyjs-answer-{{questionNumber}}{{addMoreName}}" class="surveyjs-select {{fieldClass}}" {{attrRequired}} {{nestedAnswer}} data-answer-root="{{progIdsJoined}}" {{attrRequiredFrom}}>'+ '{{optionsHtml}}'+ '</select>' } });
Custom Template for Textareas
SURVEY.setup({ url: 'json/survey.json', templates: { textarea: '<div class="surveyjs-single-answer surveyjs-answer-textarea">'+ '<textarea id="{{answerCode}}" data-answer-id="{{answerId}}" {{nestedAnswer}} name="surveyjs-answer-{{questionNumber}}" {{attrRequired}} class="surveyjs-textarea {{fieldClass}}" {{answerMaxlength}} rows="6" placeholder="{{answerPlaceholder}}"></textarea>'+ '</div>' } });
If you find a bug, please report it here