Customizing Survey Widget Behavior (JavaScript and CSS)

CustomizeSurveyBehavior.nitro_s

I was presented a scenario that I thought would be good to share.  The implementation is a bit more complex then I would like it to be but as a product team we will continue to see how we can make tasks like this easier with less need for coding.

Scenario

- Employee assesses themselves by answering survey questions and submits to their Manager
- Manager can then change any answer based on their impression of the Employee and submits the form back to the Employee
- The Employee can then review any of the changes that the Manager made as they will be highlighted in red.

I am going to go through how this was implemented as I feel it may be valuable to some for future apps that they want to build as the techniques used are transferable. 

CSS

The first thing I had to do was validate that I could modify the styles of the Survey widget.  Nearly every item in FEB can be assigned a custom CSS class name that will be applied at run-time.  So I set to work on figuring out the right CSS class to achieve the desired result:


/* Changes the backgound of a Survey question */
.lfMn .lfFormFieldSurvey .lfFormFieldSurveyTable tbody tr.changedByManager td
{
    background-color: lightcoral !important;
}

Below is the change that this CSS will make to my survey question:

Now, I only want to make this CSS change if the form is in the Manager stage and the answer selected is different from what the employee initially entered.  Now we get into the real meat of the sample!

Changing the Survey Question Style

I need to apply the custom CSS class name to a survey question if the form is in the manager stage and the answer is not what the employee initially entered.  So I needed to attach some code to the onItemChange event of each survey question.  I could have just put the following code in each survey question:

if(theItem.getBO().getCurrentStage() === "ST_Manager") {
  theItem.addClasses("changedByManager");
}

If you are going to implement a long survey with lots of questions then this technique could very quickly become a management nightmare. For example, if you have to make any changes to the code then you would have to do it in all of the questions.  So instead I chose to dynamically attach some code to each question in the survey object.  To do this I borrowed some code that I had written months before, a recursive function that walks through all the items of a "container" (form, page, section, survey, etc).  I added the processItemhasItems and getItem functions to the Settings...Events...Custom Actions section of the form.  Then I needed to define what I wanted to do once I found an item in the survey "container".  Below is the code for my processItem, which I called attachOnChangeEvent:


app.getSharedData().attachOnChangeEvent = function(item) {

//define a new object that will contain the function that performs the highlighting
var highlighterObj = {
 highlightRow: function(item) {

//If the form is currently in the Manager Review Stage
if(item.getBO().getCurrentStage() === "ST_Manager") {

    //Compare the current value against those that have been stored as the original Employee answer
    if(item.getValue() !== get(app.getSharedData().surveyAnswers, item.getId())) {
      item.addClasses("changedByManager");  //add the class name - this applies the custom style
      //check the array where we are storing the manager changes to make sure it doesn't already contain the item selected

      if(app.getSharedData().changedAnswers.indexOf(item.getId()) === -1) {  // survey registers event twice when changing answer...
        app.getSharedData().changedAnswers.push(item.getId()); //add the question ID to the list to keep track that the manager changed it
        app.getSharedData().writeListToFieldValue(item.getBO().F_changedItems, app.getSharedData().changedAnswers, false); //store all the manager changes in a hidden field
      }
    } else {

      //in all other cases we need to make sure we remove the custom class that might have been added.
      item.removeClasses("changedByManager");

      //remove the question from the answers that will be remembered
      app.getSharedData().changedAnswers = app.getSharedData().removeItemByKeyFromList(app.getSharedData().changedAnswers, item.getId());

      //write out all the items that will be remembered to the hidden field
      app.getSharedData().writeListToFieldValue(item.getBO().F_changedItems, app.getSharedData().changedAnswers, false);
    }
  }
 }
};

//Creates a listener, which will run the code when the items onItemChange event is triggered
item.connectEvent("onItemChange", dojo.hitch(highlighterObj, "highlightRow", item));
}


Now, there is a lot going on in here and I will try to explain it.  The first thing was to create an object that will be used to perform the highlighting.  Now I had to implement this in this specific manner because I had to have a way of passing the correct survey question to be highlighted.  Originally I had tried just connecting the event directly within the loop of the getItem function but what I found is that the item moves out of scope and is lost.  By using dojo.hitch() I can pass the item I want to process!

Next I had to define work to be performed when the form was in the Manager Review stage and then work for all other stages.

Keeping Track of the Employee Answers

I needed a way to record all the Employee answers so that if the Manager changes the answer but then sets it back it won't be seen as a change.

All of the work is triggered in the onLoad event of the form:

1. I set the field where I store the changed items to invisible

2. I call the recursive function to attach all the onItemChange events to each question in each survey item.

3a. If we are in the Manager stage then I need to record all the Employee answers so that I can use them to compare later on

3b. If we are in any other stage then I need to compare the current answer against what was stored to see if the custom style should be applied.


//1. hide the field that is used to track changes
form.getPage('P_NewPage').F_changedItems.setVisible(false);

//2. loop through all the questions and attach an onItemChange listener to change the css
app.getSharedData().getItem(form.getPage('P_NewPage').F_Survey, app.getSharedData().attachOnChangeEvent);
app.getSharedData().getItem(form.getPage('P_NewPage').F_Survey0, app.getSharedData().attachOnChangeEvent);

//3a. keep track of any changes that the manager may make and keep them so that
//we can process them in later stages
if(BO.getCurrentStage() === "ST_Manager") {
  app.getSharedData().getItem(form.getPage('P_NewPage').F_Survey, app.getSharedData().storeItemInArray);
  app.getSharedData().getItem(form.getPage('P_NewPage').F_Survey0, app.getSharedData().storeItemInArray);
} else {
  //3b. process any answers that were changed
  if(BO.F_changedItems.getValue() !== "") {
    var changedItems = BO.F_changedItems.getValue().split(",");
    //add the CSS class to each item
    for (var i=0;i<changedItems.length;i++) {
      var surveyItem = get(form.getPage('P_NewPage'), get(changedItems, i));
      surveyItem.addClasses("changedByManager");
    }
  }
}

Note: Any class names applied at run-time will not be persisted, which means if you submit the form those changes are lost.

Helper Functions

I created some helper functions for certain operations that needed to be performed more than once: removeItemByKeyFromList, writeListToFieldValue and storeItemInArray.

/*
* Removes the specified option from the list even if it is not the first,
* will also remove any duplicate options
*
* theList - the List item from which to remove
* key - the item to be removed from the list
*
* returns a new list with without the specified item
*/
app.getSharedData().removeItemByKeyFromList = function(theList, key) {
  //check all options for a blank value and remove it...
  var newList = new Array();

  //loop through the current list
  for(var j=0; j<theList.length;j++) {
      var curItem = get(theList, j);
      if(curItem !== key) {
          newList.push(curItem);
      }
  }
  return newList;
}

/*
* Writes stuff to the specified field value
* theItem - the UI object of a form item to write to
* theList - the array to print to theItem
* append - true/false - if true, keep the existing value of theItem or start fresh
*/
app.getSharedData().writeListToFieldValue = function(theItem, theList, append) {
  var s = "";

  if(append === "" || append === null) {
    append = false;
  }

  if(append) {
    s += theItem.getValue();
  }

  s += theList.toString();
  theItem.setValue(s);
}

/*
* Stores the value of an item in a global array
* item - the UI object of a form item
*/
app.getSharedData().storeItemInArray = function(item) {
  set(app.getSharedData().surveyAnswers, item.getId(), item.getValue());
}