SPServices Stories #9: Developing with Client-side Technologies: jQuery, REST, SPServices and jsRender

Introduction

Here’s another SPServices Stories post that is a bit older and talks about how you can use SPServices with other popular plugins and frameworks. While this is possible with the CSOM, I’ve always found that SPServices provides a much tighter and more controllable syntax. You probably can’t trust me on that, though, since I wrote it.

Phil Harding’s post from March 2012 called SharePoint: Developing with Client-side Technologies: jQuery, REST, SPServices and JsRender caught my eye because in it he explains how he used the popular JsRender with SPServices. Many people want to use rendering frameworks in general or specifically in conjunction with SPServices, and JsRender has been a popular choice. Knockout,js is probably becoming more popular, but JsRender still has its place. You should evaluate the options based on your own business requirements to decide what’s best for you.

Keep in mind that is doesn’t really matter what version of SharePoint you’re working with, as these techniques can work well with SharePoint 2007, 2010, and 2013.

Phil Harding (@phillipharding) is an independent SharePoint consultant from Manchester, UK, specializing in development for the 2007, 2010 and (he’s hoping really really soon) 2013.

Developing with Client-side Technologies: jQuery, REST, SPServices and JsRender

Having recently been immersed in developing client-side functionality for a SharePoint 2007 project I thought I’d share some of the techniques and tools I’ve used in doing so.

The functional requirements in this project are pretty standard CRUD operations;

  1. Collect data from the user and create new list items
  2. Display and modify existing data
  3. Display formatted lists of existing data

In this post we are going to retrieve a set of list items from a SharePoint list and display that data using a technique very similar to that used by ASP.NET data bound/templatized controls.

SPServices.

The premier tool IMHO for interacting with SharePoint 2007 from the client is the SPServices library by @sympmarc, check out the documentation as the library is pretty extensive.

Here we will be using the SPServices GetListItems method which wraps the SharePoint Lists web service.

REST.

If you’re working with SharePoint 2010, you might also consider using the REST interface for retrieving data, although this interface by default returns XML, it can also be configured to return JSON, and the jQuery getJSON() method does exactly that by configuring the $.ajax() call to the REST interface appropriately. Why JSON? Well as with SPServices, using JSON encoded results fits very nicely when using jsRender to render your output markup. Some good examples of how to use the REST interface can be found here.

[important]Ed: Note that as of v0.7.1, SPServices contains a function called $().SPXmlToJson which makes it a no-brainer to convert GetListItems results to JSON.[/important]

Choosing a Client Side Data Retrieval Interface

If you’re using SharePoint 2007, your options are (very) limited. By limited I mean, that pretty much your only sane option is SPServices, there are other options of course, 2007 has the [SOAP] List webservice which you could grok against – you’ll have to write a lot of javascript if you want to go that route, but seriously, why bother, SPServices has you more than covered.

If you’re using SharePoint 2010 or 2013, you’ve got the JSOM interface which you can use to read list data, frankly though, given the amount of code you have to write just to read some data, I almost never use the JSOM for this.

SPServices of course is a valid choice to make – as with all things choose the right tool to accomplish your goal. Personally, I tend to use REST when my data needs are typically one-way, i.e. reading, even then I may defer to SPServices if my query is sufficiently complex – the REST interface is pretty damn flexible but it won’t allow you to model complex queries in the way that CAML does.

For updating data back to lists, again you can use REST, JSOM or SPServices. For me, I might typically use JSOM to update list items, sure I can do this using REST, but to do this you’ll have to manage the ETag – another piece of state to maintain (see the Concurrency Management section for more information). I also came across a javascript library recently called data.js, which seems to be a wrapper over OData services, I haven’t used it yet but it definitely looks interesting.

Finally, SPServices has the UpdateListItems method which serves both creating and updating data.

jsRender.

Having retrieved your data from SharePoint, you could of course, write quite a bunch of grungy JavaScript and DOM code to create the rendered display, however there is a much better way, and that is to use something latterly called jQuery Templates, which, like ASP.NET data-bound or templatized controls, allows you to write the markup template for your output and include bits which bind to the data being displayed.

jQuery Templates are officially deprecated in favour of a new but similar technology called jsRender by Boris Moore.

jsRender seems to be gaining a lot of traction and there is a growing amount of information available out there, and the documentation provided by Boris is more than enough to get you going.

Retrieving List Items.

So lets get going, first we’ll write the SPServices code to retrieve a set of list items, which we’ll then iterate over and place into an array of JSON objects (required by jsRender).

function GetListItemData() {
  var container=$('#container');
  $(container).html("");

  // get list data
  var datarows = [];
  $().SPServices({
    operation: "GetListItems",
    webURL:"http://sp2007/sites/demo",
    async: false,
    listName: "The List Title",
    CAMLViewFields:"",
    CAMLQuery:"........",
    completefunc: function(xData, Status) {
      $(xData.responseXML).SPFilterNode("z:row").each(function() {
        var x={
          title: $(this).attr('ows_Title'),
          id: $(this).attr('ows_ID'),
          reviewoutcome: $(this).attr('ows_ReviewOutcome')
        };
        datarows.push(x);
      });
    }
  });
  ..... // display list data
}

Here we use SPServices GetListItems to retrieve the data, amongst other things we supply the following parameters;

  1. operation: “GetListItems”
  2. Optional webURL parameter, if not supplied SPServices will use the equivalent of SPContext.Current [the current Web]
  3. listName: “The List Title”
  4. CAMLViewFields: the list of fields we want returned by the query
  5. CAMLQuery: the CAML query in the form; “<Query><Where>….</Where><OrderBy>….</OrderBy></Query>”

I also specify a completion function which parses and iterates over the returned rows, creates a JSON object and adds it to an array.

Note: SPServices also provides the SPXmlToJson function which will convert the returned rows to an array of JSON objects.

Next we’ll use jsRender to display the data we just retrieved using the supplied template (#reviewTemplate), in this case the template with id reviewTemplate.

function GetListItemData() {
  var container=$('#container');
  $(container).html("");

  var datarows = [];

  ..... // get list data

  // display list data
  if (datarows.length > 0) {
    $(container).html( $("#reviewTemplate").render(datarows) );
  } else {
    $(container).html("There are no items to display.");
  }
}

The return value from jsRender is then set as the html value of a container DIV or other element.

jsRender Templates.

So what’s a jsRender template? Quite simply it is a <SCRIPT /> block with an ID value and a type set to text/x-jsrender, as shown below.

<script id="reviewTemplate" type="text/x-jsrender">
<TABLE cellSpacing="0" cellPadding="0" width="100%">
  <TBODY>
    <TR>
      <TD style="width:400px">
        <A title="{{:title}}" href="{{:~reviewurl(id)}}">{{:title}}</A>
      </TD>
      <TD style="width:20px">
        {{outcome reviewoutcome /}}
        {{if reviewoutcome=='Undecided' tmpl='#reviewundecided' /}}
        {{if reviewoutcome=='Accepted' tmpl='#reviewaccepted' /}}
        {{if reviewoutcome=='Rejected' tmpl='#reviewrejected' /}}
      </TD>
    </TR>
  </TBODY>
</TABLE>
</script>
<script id="reviewundecided" type="text/x-jsrender">
  <IMG style="" title="this is undecided." border="0" src="http://sp2007/_layouts/images/erg-0.gif" />
</script>
<script id="reviewaccepted" type="text/x-jsrender">
  <IMG style="" title="this was accepted." border="0" src="http://sp2007/_layouts/images/erg-2.gif" />
</script>
<script id="reviewrejected" type="text/x-jsrender">
  <IMG style="" title="this was rejected." border="0" src="http://sp2007/_layouts/images/erg-1.gif" />
</script>

Here we’re showing 4 jsRender templates, reason being that a feature of jsRender allows you to use template composition, i.e. to use different templates to render different parts of the output. In this case we have 3 different templates to render an <IMG…/> element according to the value of the “reviewoutcome” object property (which is the listitems ows_ReviewOutcome column value). To summarise whats going on I’ll briefly describe each of the jsRender features we’re using;

Expression Evaluation / Output an Object Property Value

Use the {{:  expression  }} tag to evaluate an expression or output the property value of the current item, this form does not perform HTML encoding of the output

Use the {{> expression  }} tag to evaluate an expression or output the property value of the current item, this form performs HTML encoding of the output

Call a Custom Helper Function

Use the {{:~ customhelper(expression)  }} tag to call a custom helper function that you have written, which optionally accepts parameters, and returns the desired output – in the sample above we created a helper function called reviewurl to build a composite HREF value for an <A /> element.

See below for help writing a custom helper function.

Conditional Processing : If/Else

Use {{if  expression }}…{{/if}} {{else  expression }}…{{/else}} tags to perform conditional processing or branching of the template – in the sample above we used conditional processing to choose a different jsRender template to build <IMG /> elements based on the value of the current items reviewoutcome property value.

Call a Custom jsRender Tag

In the above example we used conditional processing to chose a jsRender template to display different <IMG /> elements, another method of doing this is to write a custom jsRender tag which will do the same thing. To use a custom jsrender tag you have written, use the following form {{mytagname expression /}}. In the sample above we created a custom tag called outcome which accepted the reviewoutcome property value of the current item and returned an <IMG /> element.

See below for help writing a custom tag function.

To create a custom helper function;

A custom helper function, as you might imagine, is simply a JavaScript function which optionally accepts parameters and returns something for display. Usefully the function can reference global variables in the page, as you can see I’m referencing global variables g_rlid and g_thispageurl.

/* jsRender helper function */
$.views.helpers( {
  reviewurl:function(fid) {
    var m="mypage.aspx?";
    m+="form=Display";
    m+="&list="+g_rlid;
    m+="&id="+fid;
    m+="&Source="+g_thispageurl;
    return m;
  }
});

To create a custom jsRender Tag function;

A custom tag function, is also a JavaScript function which optionally accepts parameters and returns something for display.

/* jsRender custom tag */
$.views.tags({
outcome: function(value) {
var ret='';
switch(value) {
case 'Undecided':
ret="<IMG style='' title='' border='0' src='http://sp2007/_layouts/images/erg-0.gif' />";
break;
case 'Accepted':
ret="<IMG style='' title='' border='0' src='http://sp2007/_layouts/images/erg-2.gif' />";
break;
case 'Rejected':
ret="<IMG style='' title='' border='0' src='http://sp2007/_layouts/images/erg-1.gif' />";
break;
}
return ret;
}
});

Putting all this together we get something that is ripe for a little CSS beautification.

http://platinumdogs.files.wordpress.com/2012/03/sps-01.png

There’s much more to go at in jsRender, including some interesting effects involving hoverstate and re-rendering of item display using dynamic templates. Hopefully the documentation (and feature-set) will continue to improve and that this post has provided a leg up should you wish to start using it.

SPServices Stories #8 – CEWP, Nintex, jQuery, SPServices and the Client API

Introduction

I’ve been keeping a list of older posts about using SPServices that I’ve seen on the InterWebz for that day when I finally got around to doing something like SPServices Stories. This one is from Dan Stoll (@_danstoll) at Nintex, and was originally published on Dan’s blog back in February, 2012 as CEWP, Nintex, jQuery, SPServices and the Client API.

To me, the fact that someone at Nintex would choose to use SPServices to accomplish something with workflows is pretty telling, in a positive way. The folks over at Nintex know what they are doing, and if they choose to use SPServices as spackle to fill in some of the gaps in SharePoint, then it’s testament to the value of it.

In SPServices Stories #7, John Liu showed an example of starting a workflow with SPServices, and here Dan gives us some of the details under the covers.

CEWP, Nintex, jQuery, SPServices and the Client API

I had a requirement that required a not so difficult solution but tricky.. The first requirement was that a webpart had to be embedded in to a publishing page layout that was used over multiple sites in the farm. The next requirement was to only show this webpart to a select group of people.. Easy right ? Using audiences should work a treat…. Wrong. . Embedding the CEWP you can still put in the audience but it doesn’t work because it isn’t in a webpart zone… Ok, so let’s put a webpart zone in.. Pop in my custom CEWP webpart.. The audience set.. but still no joy.. Audience is working but because my webpart is in a webpart zone, it doesn’t show up.

So now I’m faced with .. “How do I get a default webpart to appear on 500 + pages that already exist?? ” Back to square one.. There has to be a way of telling a Div to hide if you aren’t part of a group of some kind. So let’s look at this.. Here is an extract of my page layout

<div class="certifiedPanel" style="display: none;">
  <div class="article-before-wp">
    <h3 class="ms-standardheader ms-WPTitle">
      <span>Details</span>
    </h3>
  </div>
  <div class="article-meta article-wp">
  <table style="width: 100%;">
    <tbody>
      <tr>
        <th class="style1" style="width: 36%;">Contact:</th>
        <td id="pubContact"></td>
      </tr>
      <tr>
        <th class="style1" style="width: 36%;">Last Modified:</th>
        <td id="modifiedOn"></td>
      </tr>
      <tr style="display: none;">
        <th style="width: 36%;">Last Review Date:</th>
        <td id="lastReviewedOn"></td>
      </tr>
      <tr>
        <th class="style1" style="width: 36%;">Next Review
        Date:</th>
        <td id="reviewedOn"></td>
      </tr>
      <tr>
        <th class="style1" style="width: 36%;">Certified Date:</th>
        <td id="certifiedOn"></td>
      </tr>
      <tr>
        <th class="style1" style="width: 36%;">HYway DOCID:</th>
        <td id="docId"></td>
      </tr>
      <tr>
        <th class="style1" style="width: 36%;">Reference ID:</th>
        <td id="refId"></td>
      </tr>
    </tbody>
  </table>Managers PanelNoneUse for formatted text, tables, and
  images.true2NormaltruetruetruetruetruetruetrueModeless
  <dir>Default</dir>Cannot import this Web
  Part.true00000000-0000-0000-0000-000000000000g_73d6de6b_445e_4d9a_8443_c05b20548336/OurProcesses/Documents/startworkflow.txt</div>
  <div class="article-after-wp"></div>
</div>

You’ll see in the first line, I have included a “display:none” style on the DIV called “certifiedPanel” The rest of it, well the first half of the Certified Panel shows a few fields from the content type, eg Modified Date, ID etc etc.. The second half is the CEWP that has a text file as it’s source of content ‘startworkflow.txt’ .. This is where part 2 of the story begins..

The other requirements is that this “Panel” had to show indicators as to the phases of the document, it also had to have a link to the version history of the document and there also had to be a couple of workflows that could be executed against the page (content type) from this panel.. Firstly, let me show you the panel

The 3 indicators shown here, show the manager at a quick glance that the document isn’t certified (Yellow) because the review date has passed, it isn’t ready for Review either (Red) as the Contact hasn’t been filled in. The only parameter that is ok is that the Review date as been set with “something” (Green)

hy_details-300x300

As you can see the document has been modified since being certified so this document is now no longer certified as changes may have been made to the content…

Ok so how did we do this.. I’ll post the full contents of the startworkflow.txt file and we can work from there. (the Startworkflow.txt) is used with the Content Editor Webpart to display on the page

[Ed: I've split the HTML and JavaScript sections for better readability.]

var $ = jQuery;
$(document).ready(SetPageIndicators);
function SetPageIndicators() {

  //get the certified and reviewed indicators
  var certifiedOn = new Date($(&quot;#certifiedOn&quot;).text());
  var hasCertDate = !isNaN(certifiedOn);
  var modifiedOn = new Date($(&quot;#modifiedOn&quot;).text());
  var hasModDate = !isNaN(modifiedOn);
  var reviewOn = new Date($(&quot;#reviewedOn&quot;).text());
  var hasReview = !isNaN(reviewOn);
  var lastReviewedOn = new Date($(&quot;#lastReviewedOn&quot;).text());
  var haslastReviewedOn = !isNaN(lastReviewedOn);
  var baseUrl = &quot;/Style%20Library/hyway/Images/&quot;;

  //set certified icon
  var certImg = $(&quot;#certImg&quot;);
  if (!hasCertDate)
    certImg.attr(&quot;src&quot;, baseUrl + &quot;statusRed.png&quot;);
  if (hasCertDate)
    certImg.attr(&quot;src&quot;, baseUrl + &quot;statusGreen.png&quot;);
  if ((hasModDate &amp;&amp; hasCertDate) &amp;&amp; (modifiedOn.setDate(+1) &gt; certifiedOn))
    certImg.attr(&quot;src&quot;, baseUrl + &quot;statusYellow.png&quot;);

  //set lastReviewedOn icon
  var lastReviewedOnImg = $(&quot;#lastReviewedOnImg&quot;);
  if (!haslastReviewedOn)
    lastReviewedOnImg.attr(&quot;src&quot;, baseUrl + &quot;statusRed.png&quot;);
  if (haslastReviewedOn)
    lastReviewedOnImg.attr(&quot;src&quot;, baseUrl + &quot;statusGreen.png&quot;);
  if (lastReviewedOn &gt; certifiedOn)
    lastReviewedOnImg.attr(&quot;src&quot;, baseUrl + &quot;statusYellow.png&quot;);

  //set review icon
  var reviewImg = $(&quot;#reviewImg&quot;);
  if (!hasReview) {
    reviewImg.attr(&quot;src&quot;, baseUrl + &quot;statusRed.png&quot;);
  } else {
    if (reviewOn &gt; new Date()) {
      reviewImg.attr(&quot;src&quot;, baseUrl + &quot;statusGreen.png&quot;);
    } else {
      reviewImg.attr(&quot;src&quot;, baseUrl + &quot;statusYellow.png&quot;);
    }
  }

  //Hide the Panel from those who don't need it
  ExecuteOrDelayUntilScriptLoaded(function () {
    var ctx = SP.ClientContext.get_current();
    var web = ctx.get_web();

    // change ID based on
    // http://devserver/OurProcesses/_layouts/people.aspx?MembershipGroupId=551
    // http://server/OurProcesses/_layouts/people.aspx?MembershipGroupId=752
    var group = web.get_siteGroups().getById(551);
    ctx.load(group);
    var users = group.get_users();
    ctx.load(users);
    var user = web.get_currentUser();
    ctx.load(user);
    ctx.executeQueryAsync(Function.createDelegate(this, function () {

        // success
        for (var i = 0; i &lt; users.get_count(); i++) {
          var u = users.get_item(i);
          //alert(u.get_loginName());
          if (u.get_loginName() == user.get_loginName()) {
            //alert(&quot;found you&quot;);

            $(&quot;.certifiedPanel&quot;).show();
          }
        }
      }), Function.createDelegate(this, function () {}));
  }, &quot;sp.js&quot;);
}
function StartWorkflow(ItemURL, WorkflowName) {
  var waitDialog = SP.UI.ModalDialog.showWaitScreenWithNoClose('working on it….', 'Please wait while Gnomes get this sorted for you...', 76, 330);

  // start the workflow manually, then take the TemplateID from the URL of the Workflow starting form
  // this is no good since it changes when you republish workflow...
  // get TemplateID first for item.
  $().SPServices({
    operation : &quot;GetTemplatesForItem&quot;,
    item : ItemURL,
    async : false,
    completefunc : function (data, status) {
      var workflowTemplateID;
      if (status == &quot;success&quot;) {
        $(data.responseXML).find(&quot;WorkflowTemplates &gt; WorkflowTemplate&quot;).each(function (i, e) {

          // hard coded workflow name
          if ($(this).attr(&quot;Name&quot;) == WorkflowName) {
            var guid = $(this).find(&quot;WorkflowTemplateIdSet&quot;).attr(&quot;TemplateId&quot;);
            if (guid != null) {
              workflowTemplateID = &quot;{&quot; + guid + &quot;}&quot;;
            }
          }
        });
      } else {

        // error can't find template
        alert(status + &quot; : &quot; + data.responseText);
        return;
      }

      // start workflow now with obtained templateID. Note this must run within the completeFunc of the first webservice call
      $().SPServices({
        operation : &quot;StartWorkflow&quot;,
        item : ItemURL,
        templateId : workflowTemplateID,
        workflowParameters : &quot;&lt;root /&gt;&quot;,
        completefunc : function (data, status) {
          waitDialog.close();
          if (status == &quot;error&quot;) {
            alert(status + &quot; : &quot; + data.responseText);
          } else {
            document.location.reload();
          }
        }
      });
    }
  });
}
function certImg_onclick() {}

function ShowVersionHistory() {
  if (!_spPageContextInfo) {
    return;
  }
  var options = {
    tite : &quot;Versions&quot;,
    url : _spPageContextInfo.webServerRelativeUrl + &quot;/_layouts/Versions.aspx?list=&quot; + _spPageContextInfo.pageListId + &quot;&amp;ID=&quot; + _spPageContextInfo.pageItemId,
    allowMaximize : false,
    showClose : true,
    width : 800,
    height : 500,
  };
  SP.UI.ModalDialog.showModalDialog(options);
}

 

<table style="width: 100%">
  <tr>
    <td>
    <img id="certImg" alt="Page Certified Indicator" src=""
    onclick="return certImg_onclick()" height="20px"
    width="20px" />Certified</td>
    <td>
      <a href="javascript:StartWorkflow(document.location, 'Super Stamp')">
      Certify Process Page</a>
    </td>
  </tr>
  <tr>
    <td>
      <div>
      <img id="reviewImg" alt="Page Review Indicator" src=""
      height="20px" width="20px" />Reviewed</div>
    </td>
    <td>
      <a href="#" onclick="javascript:ShowVersionHistory();">View
      Modification History</a>
    </td>
  </tr>
  <tr>
    <td>
    <img id="lastReviewedOnImg"
    alt="Page Ready for Review Indicator" src=""
    onclick="return lastReviewedOnImg_onclick()" height="20px"
    width="20px" />Ready for Review</td>
    <td>
      <a href="javascript:StartWorkflow(document.location, 'ReadyForReview')">
      Ready for Review</a>
    </td>
  </tr>
</table>

As you can probably guess there is are a couple of parts to this txt file..

  1. The first being jquery switching the indicators depending on the rules set and the values of certain date fields.
  2. The second part is using the client API where we are setting the ID of the SP group (note you can’t have any nested SP or AD groups here) that has access to this panel.. This then sets the DIV certifiedPanel display to show.
  3. Part 3 is using SPServices http://spservices.codeplex.com/ to call Nintex Reusable Workflows that are bound to the custom content type that I am using for these Publishing Pages. As Nintex Workflow use the same infrastructure as SharePoint workflows, the rich feature set of SPServices, allow me to start my Nintex Workflows and a whole host of other things.

These workflows not only update the Page in question but also assign tasks to the review committee, and maintain a centralised list of all pages that are currently under review, / are due for review or don’t comply to the companies guidelines..

On approval of the task, the pages are certified within the companies “Processes” site. The JavaScript indicators reflect its new status, and the document is removed form the centralised management list.

SPServices Stories #7 – Example Uses of SPServices, JavaScript and SharePoint

Introduction

I’ve been aware that John Liu (@johnnliu) is a fan of SPServices for some time now. He occasionally tweets about things he’s up to, and they always sound intriguing.

Recently, I asked him if he’d like to share any of his own SPServices Stories, and he did a post for me on his blog. In the post, John shows three great examples of how you can slide SPServices under some truly impressive functionality that greatly improves the overall SharePoint user experience.

There’s another SPServices Story coming up from Dan Stoll (@_danstoll) which goes into some of the details on John’s first example below.

Without further ado, here’s John’s first SPServices Story. I expect that John will have a few more SPServices Stories to share with us over time.

Example uses of SPServices, JavaScript and SharePoint

I wanted to write about spservices.codeplex.com from Marc D Anderson – we’ve found ourselves using this really special library time and again across different projects to talk back to SharePoint quickly.

Starting Workflows

Here’s a page from one of our Process Wiki articles.

Process Wiki Example

  • We have a special “Contributor-only” Web Part on the right.
  • It shows the various workflow status’ on the current page, as traffic light bubbles.
  • The “Certify Process Page” calls a JavaScript function that calls StartWorkflow via SPServices.
  • The workflow is a Nintex workflow and triggers a significant multistage approval process.  But you can use StartWorkflow to start SharePoint workflows as well.

Getting List Data, Lots of List Data

Here’s our task list, represented as a task board.

Task Dashboard

  • This one is completely done with SPServices to get the list items
  • Convert the objects to JSON using SPServices.SPXmlToJson
  • Then binding the objects to UI via Knockout
  • There’s jQuery UI’s drag and drop in play, so we can change the Task’s status by dragging the task across a different column.
  • Update task using SPServices’ UpdateListItems call.
  • And some nice CSS.
  • This particular page also runs via SharePoint 2010′s OData listdata.svc, but is completely viable with SPServices on SP2007 as well.

Getting User Profiles via Search

Here’s our People page.

People Page

  • First, get SharePoint to index your people.
  • Use SPServices to call SharePoint search to return a bunch of people, including their picture (one would say, especially their picture).
  • Here I use Knockout to render the pictures.  When clicked, each one opens that user’s My Site page.
  • There’s a filter box on the top right, as well as “fake” refinements on the left hand side that allows us to re-query SharePoint search for filtered people.
  • One possible idea here would be to use SPServices’ User Profile Service support and talk directly to the User Profile service if you want to skip the search service.

Summary

A quick post of 3 recent JavaScript customizations that heavily used SPServices.  Hope that give you guys a lot of ideas.  Let me know what you guys think.

SPServices Stories #6 – Custom Quizzing System

Introduction

Many times, SPServices Stories play out on the field of the Codeplex Discussions. As someone posts questions about how to build some of the components of their solution, the overall plan comes into focus.

Over the last week or so, I’ve been helping Oli Howson (@Mr_Howson) in the discussions here and here. As the bits and pieces came to together for me, I thought that Oli’s work in progress would make a great SPServices Story, especially since he took the trouble to write up what he was trying to accomplish in the discussion thread.

Oli’s project is in process, and it’s certainly possible that he will make changes from here. However, I thought it would be useful to post things “as-is” so that everyone could see how he’s going about it. If he makes any significant changes, we’ll try to post them back as well.

If you have any comments for Oli about how you might do things differently, I’m sure he’d be interested. I know I would be.

Oli’s entire bespoke page is shown below, so you get to see the markup, the script, everything.

Custom Quizzing System

I am a teacher – running the ICT and Computer Science department of a South London Academy – we teach both disciplines to 11-18 year olds. For key-stage 3 (Y7, 8 and 9) we have for the last few years set homework on our VLE (Virtual Learning Environment) which had a built-in testing system. Due to that being about the only part of the VLE that wasn’t naff, it was recently retired and a new SharePoint system brought in. It’s got a few bespoke bits, and I am not an admin. Now I’m faced with the dilemma: I have 555-ish students needing their homework setting every week, I deliver all my learning resources via the SharePoint system, but there is no built-in quizzing system. Yes – there are surveys, but they don’t mark and have their own foibles. So I built this JavaScript-based quizzing system.

Methodology

The teacher in charge of setting homework that week creates a multiple-choice quiz on a stand-alone piece of client software I wrote in Delphi. This then creates an array string which it pastes into the quiz template (with a relevant name) and copies the file to the relevant document store in the SharePoint server. The teacher then just creates a link from the homework page to the relevant quiz, and when the kids hit the quiz the results go into the relevant list (created with the same name as the quiz). The difficult bit was making sure that the list was created the first time the quiz is run. The idea is the teacher hits the quiz once the link is up to make sure it has worked. When they submit, it creates the list, adds the columns, and updates the view. The second time it tests if the list exists (it does now!) and just inserts their score, which it also shows to the kids and then closes the window.

Well, I think I’m there! I’m going to get this beta tested by a group of Year 9 students tomorrow, but I’ll put the code below for reference to anyone that might find it interesting. I’m sure I’ve made loads of faux-pas as I’ve written a grand total of about three things in JavaScript, and have very limited knowledge of SharePoint.

Wheeee :)

[important]Code update with Oli’s changes in version 1.1.0 on 2013-02-14[/important]

<!doctype html>

<html lang="en">
<head>
 <meta charset="utf-8" />
 <title>jQuery UI Tabs - Default functionality</title>
 <link rel="stylesheet" href="/mertonshared/computerstudies/homework/Documents/includes/jquery-ui.css" />
 <script language="javascript" src="/mertonshared/computerstudies/homework/Documents/includes/jquery-1.8.1.js"></script>
 <script language="javascript" src="/mertonshared/computerstudies/homework/Documents/includes/jquery-ui.js"></script>
 <script language="javascript" src="/mertonshared/computerstudies/homework/Documents/includes/jquery.SPServices-0.7.2.js" type="text/javascript"></script>
 <script>
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // Quizzer v1.0.2                                                                                //
 // Change Log:                                                                                   //
 //  - 1.1.0 Added check whether user has completed before - one try only. Reordered some actions //
 //          Added permissions setting when list is being created. Changed submit button value.   //
 //  - 1.0.2 Removed some pointless alerts for ordinary users, reenabled windows.close(), added   //
 //           some useful comments.                                                               //
 //  - 1.0.1 Added view change to show all fields                                                 //
 // Currently available:                                                                          //
 //  - Multiple choice                                                                            //
 //  - Unlimited questions                                                                        //
 //  - Four options per question                                                                  //
 // Future Plans                                                                                  //
 //  - Varying number of options per question                                                     //
 //  - Missing word completion                                                                    //
 //  - Include YouTube clip                                                                       //
 ///////////////////////////////////////////////////////////////////////////////////////////////////

 var startseconds;
 var filename;
 var score;
 var thisUserName;
 var previouslytried;

 function checkiflistexists() {
    //alert('checking existance of list: '+filename);
    $().SPServices ({
        operation: "GetList",
        listName:  filename,
        completefunc: function (xData, Status) {
                //tp1 = xData.responseText;
                //alert(tp1);
                //alert(Status);
                if (Status == 'success') {
                    //alert ('Exists - dont create - just insert data');
                    insertdata();
                }
                else {
                    alert('Dont exist');
                    $().SPServices({
                        operation: "AddList",
                        listName: filename,
                        description: "List created for quiz: "+filename,
                        templateID: 100,
                        completefunc: function (xData, Status) {
                            alert('Trying to create list');
                            alert(Status);
                            alert('Now add fields');
                            var nfields = "<Fields><Method ID='1'><Field Type='Text' DisplayName='Score' ResultType='Text'></Field></Method><Method ID='2'><Field Type='Text' DisplayName='TimeTaken' ResultsType='Text'></Field></Method><Method ID='3'><Field Type='Text' DisplayName='CompletedOn' ResultsType='Text'></Field></Method></Fields>";
                            $().SPServices({
                                operation: 'UpdateList',
                                listName: filename,
                                newFields: nfields,
                                completefunc: function(xData, Status) {
                                    tp1 = xData.responseText;
                                    tp4 = tp1.indexOf("errorstring");
                                    if (tp4 < 0) {
                                        alert("Fields created! - Update View");
                                        var viewname = "";
                                        $().SPServices({
                                            operation: "GetViewCollection",
                                            async: false,
                                            listName: filename,
                                            completefunc: function (xData, Status) {
                                                alert('Complete Func - GewViewCollection');
                                                $(xData.responseXML).find("[nodeName='View']").each(function() {
                                                    var viewdisplayname = $(this).attr("DisplayName");
                                                    if (viewdisplayname=="AllItems") {
                                                        viewname = $(this).attr("Name");
                                                        return false;
                                                    }
                                                });
                                            }
                                        });
                                        alert('Ok - done GetViewCollection - now update the view');
                                        var viewfields = "<ViewFields><FieldRef Name=\"Title\" /><FieldRef Name=\"Score\" /><FieldRef Name=\"TimeTaken\" /><FieldRef Name=\"CompletedOn\" /></ViewFields>";
                                        $().SPServices({
                                            operation: 'UpdateView',
                                            async: false,
                                            listName: filename,
                                            viewName: viewname,
                                            viewFields: viewfields,
                                            completefunc: function(xData, Status) {
                                                alert('Trying to update view');
                                                alert(Status);
                                                alert('Updated view - now update permissions');
                                                //insertdata();
                                                $().SPServices({
                                                    operation: 'AddPermission',
                                                    objectType: 'List',
                                                    objectName: filename,
                                                    permissionIdentifier: "HARRISNET\\ham-grp-students",
                                                    permissionType: 'user',
                                                    permissionMask: 1011028719,
                                                    completefunc: function(xData, Status) {
                                                        alert('Trying to update permissions');
                                                        alert(Status);
                                                        alert(xData.responseXML.xml);
                                                        alert('Done... hopefully! - better insert the data!');
                                                        insertdata();
                                                    }
                                                });
                                            }
                                        });
                                    }
                                    else {
                                    // Error creating fields!
                                    alert("Error creating fields!");
                                    }
                                }
                            });
                        }
                    });
                }
            }
        });
 }

 function insertdata() {
    var endseconds = new Date().getTime() / 1000;
    endseconds = endseconds - startseconds;
    var d = new Date();
    var dd = d.toDateString();
    var dt = d.toTimeString();
    $().SPServices({
        operation: "UpdateListItems",
        async: false,
        batchCmd: "New",
        listName: filename,
        valuepairs: [["Title", thisUserName],["Score",score],["TimeTaken",Math.round(endseconds).toString()+" seconds"],["CompletedOn",dd+" "+dt]],
        completefunc: function (xData, Status) {
            //alert('Trying to add data');
            if (Status == 'success') {
                //inserted();
            }
            else {
                alert(Status+' : There was a problem inserting your score into the database. Please notify Mr Howson!');
                //inserted();
            }
        }
    });
    alert('You achieved a score of '+score);
    window.close();
 }

 function checkanswers() {
    var form = document.getElementById('answers');
    score = 0;
    for (var i = 0; i < form.elements.length; i++ ) {
        if (form.elements[i].type == 'radio') {
            if (form.elements[i].checked == true) {
                if (questions[form.elements[i].name.substring(9)-1][questions[form.elements[i].name.substring(9)-1].length-1] == form.elements[i].value)
                {
                 score++;
                }
            }
        }
    }
  $(document).ready(function() {
    checkiflistexists();
  });
 }

 function initialise() {
    var rowcount = 0;
    startseconds = new Date().getTime() / 1000;
    var url = window.location.pathname;
    thisUserName = $().SPServices.SPGetCurrentUser({
        fieldName: "Title",
        debug: false
    });
    filename = url.substring(url.lastIndexOf('/')+1);
    filename = filename.substring(0,filename.lastIndexOf('.'));
    //alert(filename);
    //alert('Getting items');
  $().SPServices({
    operation: "GetListItems",
    async: false,
    listName: filename,
    CAMLViewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='Score' /></ViewFields>",
    CAMLQuery: "<Query><Where><Eq><FieldRef Name='Title' /><Value Type='Text'>"+thisUserName+"</Value></Eq></Where></Query>",
    CAMLRowLimit: 1,
    completefunc: function (xData, Status) {
      $(xData.responseXML).SPFilterNode("z:row").each(function() {
        //var liHtml = "<li>" + $(this).attr("ows_Title") + "</li>";
        score = $(this).attr("ows_Score");
        //$("#tasksUL").append(liHtml);
        rowcount++;
      });
    }
  });
    //alert(rowcount);
    //alert('done');
  if (rowcount > 0) {
   previouslytried = true;
   document.getElementById('tabs').style.visibility = 'hidden';
   alert('Sorry - you have already tried this quiz - one try only!\nLast time you got a '+score);
   window.close();
  }
  else {
   previouslytried = false;
  }
 }

 function inserted() {
    //window.close();
 }

 $(function() {
  $( "#tabs" ).tabs();
 });

 var questions = new Array;
  //The section below should be uncommented when not testing - this will be replaced by the client
  // side application with the questions array.
  [INSERTQUESTIONS]

 //The following questions can be uncommented for testing purposes
 //questions[0] = ['q1','a','b','c',1];
 //questions[1] = ['q2','d','e','f',2];
 //questions[2] = ['q3','g','h','i',3];
 </script>
</head>

<body onload="initialise()">
 <div id="tabs">
  <ul>
   <script language="JavaScript">
    for (var i = 0; i< questions.length; i++)
    {
     document.write('<li><a href="#tabs-'+(i+1)+'">Question '+(i+1)+'</a></li>');
    }
    document.write('<li><a href="#tabs-'+(i+1)+'">Summary</a></li>');
   </script>
  </ul>
   <form name="answers" id="answers">
   <script language="JavaScript">
    for (var i = 0; i < questions.length; i++)
    {
     document.write('<div id="tabs-'+(i+1)+'">');
     document.write(' <p>'+questions[i][0]+'</p>');
     for (var j = 1; j < questions[i].length-1; j++)
     {
      document.write(' <p><input type="radio" name="question-'+(i+1)+'" value="'+j+'">'+questions[i][j]+'<br></p>');
     }
     document.write('</div>');
    }
    document.write('<div id="tabs-'+(i+1)+'">');
    document.write(' <p><input type="submit" onclick="checkanswers(); return false;" value="Submit Homework"></p>');
    document.write('</div>');
  </script>
  </form>
 </div>
</body>
</html>