Writing a Survey ID to a List on Response Creation (without Workflow)

SharePoint surveys don’t fire a workflow. You can create one, but it will never fire.

And the reason is fairly understandable. When should it fire? When is the survey item created? When is it changed? To understand how difficult it is to answer those questions, I need to take a moment to explain how surveys work.

Surveys can take two forms, branched or unbranched. An unbranched survey looks a lot like any other list and functions almost exactly like it (except for workflows).

But, a branched survey is a completely different proposition.

When you have a branched survey, the questions are presented a few at a time and depend on the answers from previous questions. This is great, especially when you have a requirement that looks like this:

Do you like ice cream? If yes, which flavor?

In other words, if the answer is No, I’m not going to ask you which flavor is your favorite.

SharePoint accomplishes this by starting with a NewForm.aspx, just like other lists, but it only shows the first branch of the survey. When the user clicks Next, the answer to the first branch is saved and an ID is assigned to the survey. The next page is displayed using EditForm.aspx and passing in two parameters to the page: the ID of the survey, and the first question of the next branch. This happens each time the user clicks Next, until the last branch, when a Finish button is displayed rather than Next.

At any point, if the user clicks Cancel on an EditForm.aspx page, she is prompted with a question about whether the entire survey should be deleted (recycled, actually) or not. So, built into the creation of the survey item is also the possibility that it will be destroyed before all questions are answered, but after some data has already been saved.

Typically, with other lists, a create workflow is fired when NewForm.aspx is saved, that is, when the item ID is created. The problem with doing that with a branched survey is that the workflow may need required fields that come later in the survey and so are not available when NewForm.aspx is saved.

Likewise, with other lists, a change workflow is fired when something changes and the EditForm.aspx is saved. Again, with a survey, EditForm.aspx is used in the creation of the item, so a save from it doesn’t necessarily mean something has changed; it may be the initial creation of the data for that column.

It’s often necessary or desirable to spawn a new task for a user to complete, which can be handled separately from the creation of the initial item. The challenge comes in when you want to easily link from the creator item to the created item and vice versa. In Understanding SharePoint Journal’s Bonus to Issue 4, I explained a method to use the Collect Date from a user workflow action to create that two-way link between a List item and a Task item using a workflow. In our case, we need the ability to ask certain questions based on the answers to other (branching), which a Task can’t do, so a Survey is a better option.

In both cases, it’s pretty useful to include a lookup column in both items (initiating item and task, or initiating item and survey response) that point to each other. With the former, you can fire a workflow on creation of the task to write its ID to the initiating item. With the latter, you can’t.

I was recently asked to create an intake list for work-related injuries and illnesses, which we are calling the Injury Report list. The simple setup allows employees to report when an injury or illness occurs at work and allows HR to see those reports for its legal documentation.

But, because the company is a healthcare provider, blood-borne pathogen exposure (BPE) is one of the possible types of injury/illness that can occur. And if it does occur, there’s an entirely separate set of additional questions that need to be documented. Many of these are similar to our ice cream question above: if yes, then answer these additional clarifying questions; if no, move on to the next question.

These additional questions were perfectly suited for a survey. But, the answers have to be tied to the original injury intake report.

So, how can we get these two to talk to each other?

Using Marc Anderson’s jQuery Library for SharePoint Web Services on Codeplex, we can get the information we need, when we need it, and write out where it needs to go.

The basic steps are:

  1. If it’s a BPE, email the user a survey response link that includes the Injury Report’s ID in the querystring.

  2. Add jQuery to the survey’s NewForm.aspx to fill the lookup on the survey with the ID of the report from the query string.

  3. Add jQuery to the survey’s EditForm.aspx to update the Injury Report with the survey ID.

We’ll use Javascript and jQuery to capture the ReportID and update our report lookup column in the BPE survey with it. Of course, that means you’ll need to add a lookup column as the first question (or at least in the first branch) of the survey so that it can link back to the Injury Report on creation. This will create a drop-down list as the first question. We’re going to use this column to connect the two, but we’re going to hide it. So, make it required and no one will be able to submit a survey without supplying a ReportID once we add a little jQuery to it.

  1. Point your browser to your survey’s NewForm.aspx (you don’t need the ReportID in the query string yet).

  2. Right-click on the page and choose View Source.

  3. While viewing the source, press Ctrl+F and find the label text for your lookup field. It should look something similar to this:

    <TR>
    <TD valign="top" width="90%" class="ms-formlabel">
            Injury Report
    </TD>
    </TR>
    <TR>
    <TD valign="top" width="90%" class="ms-formbodysurvey">
    <!-- FieldName="Injury Report"
            FieldInternalName="ReportID"
            FieldType="SPFieldLookup"
        -->
    <span dir="none"><select name="ctl00$m$g_a31f7024_8309_422f_9bba_84a452812e43$
                                   ctl00$ctl01$ctl00$ctl00$ctl00$ctl04$ctl00$Lookup" 
                      id="ctl00_m_g_a31f7024_8309_422f_9bba_84a452812e43_ctl00_ctl01_
                          ctl00_ctl00_ctl00_ctl04_ctl00_Lookup" title="Injury Report">
    <option selected="selected" value="0">(None)</option>
    <option value="10">Migraine</option>
    <option value="9">Needle stick</option>
    <option value="15">Sprained ankle</option>
    <option value="14">Threw back out</option>
    </select><br/></span>
    </TD>
    </TR>
  4. Notice that the select control has a title that matches your label (Injury Report). That’s what we’re going to be searching on with jQuery.

  5. Add ?PageView=Shared&ToolPaneView=2 to the URL in your browser to put it in Edit mode.

  6. Add a hidden CEWP to the bottom of the page.

  7. Paste the following into the webpage dialog:

    <script src="/scripts/jquery.min.js" type="text/javascript"></script>
    <script type="text/javascript">
      $(document).ready(function() {
        $("input[value='Save']").hide(); // hides the Save buttons
        $("table.ms-formtoolbar tr:first").hide(); // hides the top toolbar
        var rptItem = $("select[title='Injury Report']"); // sets a variable to the select control 
       //with our ID in it
        rptItem.hide(); //hides the select control
        fillDefaultValues("Injury Report","ReportID"); // fills the select control with the data 
       //from the querystring
        var rptText = $("select[title='Injury Report'] option:selected").text(); // sets 
       //a variable to the text of the selected item
        rptItem.after(rptText); // prints the text of that variable after the select control 
       //(which is now hidden)
      });
    
    function fillDefaultValues(fieldName,value) {
        // Notice that you call fillDefaultValues, passing it: a) the Display Name of the 
       //column, and b) the querystring key
    JSRequest.EnsureSetup();
    var qs = JSRequest.QueryString[value],
    x = $('select[title="'+fieldName+'"]');
    if (typeof(qs)=="undefined") {
    x.val(0)
    }else {
    x.val(qs)
    };
    x.change(function () {
    x.val(qs);
    }).change();
    }
    </script>

    fillDefaultValues captures the query string value and puts it in the control matching the title supplied.

So far, what we’ve done is pass the Injury Report’s ID to the BPE survey response via an email link. Steps 1 and 2 are complete.

Here’s where we really put Marc’s library to work. We’re going to use his web services library to update the item in the Injury Report list by writing the ID of this BPE survey response to the BPE survey lookup field (make sure you’ve created that column) in the Injury Report. We’ll do this on the EditForm.aspx, since the ID of the survey response isn’t available on NewForm.aspx (it hasn’t been created yet).

Follow the steps above for adding a CEWP to the EditForm.aspx and getting the basic jQuery shell in place. From there, we’ll start putting the pieces in place to make the final connection between these two items. Include the snippets for hiding the select control and displaying the text, as well as hiding the Save button and top toolbar (when we come back to the record in Edit mode, we’ll want that same functionality to be in place). fillDefaultValues is not needed on the EditForm.aspx, however, because the connection will have already been made.

Looking through the library, we want to use the UpdateListItems operations of the Lists web service.

$().SPServices({
   operation: "UpdateListItems",
   async: false, // This ensures that processing waits for the response
   listName: "Injury Report", // Use the Display Name of your Source List
   updates: "<Batch OnError='Continue'>" +
     "<Method ID='1' Cmd='Update'>" +
      "<Field Name='SurveyID'>" + SurveyID + "</Field>" +
            // The first SurveyID is the StaticName of the lookup column in the Injury Report;
            // The second SurveyID is a variable we'll define in a minute
      "<Field Name='ID'>" + ReportID + "</Field>" +
            // ID is the column name of the ID of Injury Report
            // ReportID is another variable we'll define shortly
     "</Method>" +
    "</Batch>",
   completefunc: function(xData, Status) {
// We could put any post-processing or error handling here
   }
  });
});

So, we need to declare a couple of variables to make the above work.

SurveyID

Since we’re on EditForm.aspx, the easiest place to get this is from the query string.

The following code snippets will do that for us:

We need to declare this variable (bold line below) before the SPServices call to UpdateListItems, so we’ll put it right after we display the Report Title:

  . . .
    rptItem.after(rptText);
    var SurveyID = getQuerystring('ID');  
    // ID is found in the page URL (e.g. ../EditForm.aspx?ID=1)
  });
  . . .

//The code snippet for getQuerystring should go last before the <script> tag closes.

 . . .
});

function getQuerystring(key, default_)
{
  if (default_==null) default_="";
  key = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
  var regex = new RegExp("[\\?&]"+key+"=([^&#]*)");
  var qs = regex.exec(window.location.href);
  if(qs == null)
    return default_;
  else
    return qs[1];
}
</script>
ReportID

The ReportID is not accessible from this page unless we’re on the first branch of the survey. When the response is recorded, the first time we’re on EditForm.aspx is on the second branch, so we need to get the ReportID some other way.

SPServices to the rescue again, in the form of the GetListItem operation. This helpful service allows us to get the XML for any list item by using common SQL syntax. First, we’ll declare the variable, making it null. This gives it a scope outside the jQuery call to SPServices. For easy maintenance, we’ll just declare it right after the SurveyID declaration:

  . . .
    rptItem.after(rptText);
    var SurveyID = getQuerystring('ID');
    var ReportID = null;
  });
  . . .

Now, we’ll call GetListItem before we call UpdateListItems, so that ReportID has data by the time we call UpdateListItems. Paste the following jQuery snippet (with edits) right before the UpdateListItems section:

$().SPServices({
       operation: "GetListItems",
       async: false,
       listName: "BPE Survey", //Again, use the Display Name here
       CAMLQuery: "<Query><Where><Eq>" +
                         "<FieldRef Name='ID' />" +
                         "<Value Type='Integer'>" + SurveyID + "</Value>" +
// This is the variable we declared earlier
                    "</Eq></Where></Query>",
       completefunc: function(xData, Status) {
          $(xData.responseXML).find("[nodeName= z:row]").each(function() {
              ReportId = $(this).attr("ows_ReportID");
                  // Notice ows_ReportID; ReportID is StaticName of the Lookup column 
              // in the Survey list;
                 // Add the "ows_" to the front of that and you will have the XML 
             // attribute you need.
          });
       }
   });

That’s it. Here are the complete snippets we created (with notes left in to help you make the changes necessary for your implementation).

NewForm.aspx CEWP source:

<script src="/scripts/jquery.min.js" type="text/javascript"></script>
<script src="/scripts/jquery.SPServices.min.js" type="text/javascript"></script>
           // Download Marc's code here http://spservices.codeplex.com;
           // Save it in a SharePoint directory on your site and modify the previous line accordingly

<script type="text/javascript">
$(document).ready(function() {
    $("input[value='Save']").hide();
    $("table.ms-formtoolbar tr:first").hide();
    var rptItem = $("select[title='Injury Report']"); // Use title shown in source in step 3
    rptItem.hide();
    fillDefaultValues("Injury Report","ReportID"); // See function below for syntax 
   //on this one
    var rptText = $("select[title='Injury Report'] option:selected").text(); 
   // Use title shown in source in step 3
    rptItem.after(rptText);

});

function fillDefaultValues(fieldName,value) {
    // Notice that you call fillDefaultValues, passing it: a) the Display Name of the column, 
   // and b) the querystring key (i.e. NewForm.aspx?ReportID=1)
  JSRequest.EnsureSetup();
  var qs = JSRequest.QueryString[value];
  var x = $('select[title="'+fieldName+'"]');
  if (typeof(qs)=="undefined") {x.val(0)}
  else {x.val(qs)};
  x.change(function () {
    x.val(qs);
  })
  .change();
}

</script>

EditForm.aspx CEWP source:

<script src="/scripts/jquery.min.js" type="text/javascript"></script>
<script src="/scripts/jquery.SPServices.min.js" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function() {
    $("input[value='Save']").hide();
    $("table.ms-formtoolbar tr:first").hide();
    var rptItem = $('select[title="Injury Report"]'); // Use title shown in source in step 3
    rptItem.hide();
    var rptText = $('select[title="Injury Report"] option:selected').text()); 
// Use title shown in source in step 3
    rptItem.after(rptText);

    var SurveyID = getQuerystring('ID');
    var ReportID = null;

    $().SPServices({
       operation: "GetListItems",
       async: false,
       listName: "BPE Survey", // Use Display Name of your survey list
       CAMLQuery: "<Query><Where><Eq>" +
                        "<FieldRef Name='ID' />" +
                        "<Value Type='Integer'>" + SurveyID + "</Value>" +
                       "</Eq></Where></Query>",
       completefunc: function(xData, Status) {
           ReportID = $(xData.responseXML).find("[nodeName='z:row']").attr
           ("ows_ReportID");
// Prepend 'ows_' to the StaticName of the Lookup column in your survey list
       }
    });

    $().SPServices({
       operation: "UpdateListItems",
       async: false,
       listName: "Injury Report", // Use Display Name of your source list
       updates: "<Batch OnError='Continue'>" +
                  "<Method ID='1' Cmd='Update'>" +
                    "<Field Name='SurveyID'>" + SurveyID + "</Field>" +
                    // Use StaticName of the Lookup column in your source list
                    "<Field Name='ID'>" + ReportID + "</Field>" +
                  "</Method>" +
                "</Batch>",
       completefunc: function(xData, Status) {
       }
    });
});

function getQuerystring(key, default_)
{
  if (default_==null) default_="";
  key = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
  var regex = new RegExp("[\\?&]"+key+"=([^&#]*)");
  var qs = regex.exec(window.location.href);
  if(qs == null)
    return default_;
  else
    return qs[1];
}

</script>

I may make it sound really easy, but figuring it all out wasn’t that straightforward. If you’d like to follow the dialog and thought process that actually led to this solution, it’s all archived at http://spservices.codeplex.com/Thread/View.aspx?ThreadId=76735.