As background story, I finished university with a graduation project for a company, which you can find it
here. The goal was to do something similar to Google Forms integrated with an ISO 9001 document management system, so I needed a good library to build surveys. It had to meet the following requirements:
- Good Documentation
- JSON-based library
- Custom CSS
- Send data to a server or at least provided a method
After intensive research, I encountered many libraries but they didn’t meet at least one requirement, except for SurveyJS. It has complete documentation with examples and provided helper methods to get data, therefore I decided to use it. After finishing the project, I thought it would nice to post the implementation applied to a production project, so let’s get started!
What is SurveyJS?
SurveyJS is a JSON-based javascript library that allows you to have full control of its use. You can modify the main object with additional functionality, send and receive data to a server at any moment, and the best part is that it is based on JSON! It’s even compatible with different javascript frameworks and libraries like React, Angular, JQuery, and Vue.
How do I use it?
In simple words, we will just need to create a simple structured object the way SurveyJS wants. The implementation I have includes the follow: integration with MongoDB simple methods to manage the data, and using SurveyJS jQuery library.
In the code below, there’s schema we will be using in the project. Most of the variables are self-explanatory and required by my project, but I want to focus on the questions array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
'use strict' var mongoose = require('mongoose'); let schema = new mongoose.Schema({ surveyName: String, business: [String], department: String, period: { start: Date, end: Date }, responsible: String, clients: [String], questions: [{ name: String, title: String, formType: String, isRequired: Boolean, inputType: String, choices: [String], clients: [String], service: String, mininumRateDescription: String, maximumRateDescription: String, choicesValue: [{ name: String, value: Number }], rateValues: [Number], showChoices: Boolean, created: Date }], uniqueResponses: Boolean, finalGrade: Number, active: Boolean, general: Boolean, created: Date }) module.exports = mongoose.model('Survey', schema); |
Building a survey
Every field declared is going to be used as it is except for formType. We will just set the field type right after getting data from MongoDB (MongoDB complains saving “type” field). After receiving data from our server, we will build a survey model and send it to SurveyJS method. Let’s create a function called buildSurvey:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
|
function buildSurvey(survey) { //STEP 1. Set each form type to type (ONLY IF BRINGING DATA FROM MONGODB) survey.questions = survey.questions.map(question => { question.type = question.formType; return question; }); //STEP 2. Group the survey by a specific field (service in my case) in order to separate the survey in pages let groupedSurvey = commonFactory.groupBy(survey.questions, 'service'); let pages = []; //STEP 3. Loop through "service" and create a page with all the questions. Push it to the pages array. Object.keys(groupedSurvey).forEach((element) => { pages.push({ questions: groupedSurvey[element] }); }); $scope.completeSurvey = survey; //STEP 4. Build the surveyModel using the pages previous built. Survey.Survey.cssType = "bootstrap"; //Sets SurveyJS default CSS as Bootstrap (to match theme) var surveyModel = new Survey.Model({ title: survey.surveyName, pages: pages }); //Configuration options for the model. surveyModel.showProgressBar = "top"; surveyModel.pagePrevText = "Atras"; surveyModel.pageNextText = "Siguiente"; surveyModel.completeText = "Completar encuesta"; surveyModel.completedHtml = "<h5>Gracias por completar la encuesta! </h5>" //STEP 5. Select main survey container and instantiate a new survey passing the model, custom CSS, and a custom method when submitting data. $(".survey").Survey({ model: surveyModel, css: surveyCustomCSS, onComplete: sendDataToServer }); } let surveyCustomCSS = { root: "survey-container", row: "row-separator", pageTitle: "sv_p_title", question: { root: "sv_q", title: "sv_q_title" }, checkbox: { root: 'sv_q_checkbox' }, radiogroup: { root: 'sv_q_radiogroup' }, rating: { root: 'sv_q_rating', item: 'sv_q_rating_item' }, navigationButton: "btn btn-primary" }; |
The function accepts an object as parameter, which is then processed to match library’s structure. The pages section is optional, you can send the questions array and it will generate a single page full of questions. We will be implementing custom CSS to match the theme used, which is based in Bootstrap. I had to do some tweaks like more spaces between page title and question and fix checkbox, radio group and rating CSS, since it looked bad with Bootstrap Alpha 4 (looked like default browser elements).
Saving a survey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
function sendDataToServer(survey) { const data = survey.data; //Builds response object with user information const response = { job: $rootScope.client.job, results: [], timestamp: new Date(), client: { _id: $rootScope.client._id, username: $rootScope.client.username } }; //Build a result for each answered question for (var question of Object.keys(data)) { //Gets selected question const selectedQuestions = $scope.completeSurvey.questions.filter(e => { return e.name === question; }); //if no questions found, break the loop if (selectedQuestions.length === 0) break; //grabs the selected question const selectedQuestion = selectedQuestions[0]; response.surveyId = $scope.completeSurvey._id; //builds a result object with the selected question and the answer const result = { service: selectedQuestion.service, question: question, formType: selectedQuestion.formType, answer: data[question], rates: selectedQuestion.rateValues }; //Questions that has a value process differently... if (selectedQuestion.formType === 'checkbox' || selectedQuestion.formType === 'dropdown' || selectedQuestion.formType === 'radiogroup') { //grabs the possible values per question const values = selectedQuestion.choicesValue.map(e => { return e.value; }); //if there are multiple question, the answer will be one concatenated string if (data[question] instanceof Array) { result.answer = data[question].join(); const answerValues = []; //set the value per answer data[question].forEach(e => { answerValues.push(values[selectedQuestion.choices.indexOf(e)]); }); //sets the joined value of all answers result.value = answerValues.join(); } else { result.value = values[selectedQuestion.choices.indexOf(data[question])] + ''; } } response.results.push(result); } $http.post('/api/surveys/responses/', response) .then(function(response) { console.log(response); }) .catch(function(error) { console.log(error); }); } |
As you may have noticed, there’ a function called “sendDataToServer” that I pass to the survey object with the goal of saving user’s response to the database. This function processes the result and builds up a response object based in the survey response database schema, which follows the idea of how Google Forms saves the data: timestamp column accompanied by all the questions columns with a row of answers. The function concludes with a post request to the node server with the response that does a batch insert to the database.
Conclusions
I hope this guide will be helpful for those who are looking for any implementation of this nice library. Let me know if you have any questions or comments regarding this topic (which I know I can write better code, time was not in my favor at that time). I will be updating this post with images previewing the working survey, also, I am planning to make a guide about the administrator section.