SPServices Stories #21 – Redirect If User Clicked a Button Previously

This entry is part 20 of 20 in the series SPServices Stories

Introduction

teylynIngeborg Hawighorst (@IngeborgNZ) is a long-time SPServices user who has come up with any number of intriguing uses for the library. I’d recommend her blog anytime if you’d like to learn about interesting things you can do with SharePoint, but even more so if Excel is your bag. Ingeborg has been an Excel MVP for years running (see her profile on the MVP site). Some of the best solutions using SPServices come out of discussions in various SharePoint-oriented forums. In this case, Ingeborg spotted some suggestions from Eric Alexander (@ejaya2 aka PirateEric) and decided to build it out. Without further ado, here is Ingoborg’s article, reposted from her blog cheers, teylyn.

Redirect If User Clicked a Button Previously

I just came across this question in sharepoint.stackexchange.com. When a user visits a SharePoint site, they are presented with a splash screen and need to accept the policy before they can proceed. Upon subsequent visits, the splash screen does not show, because SharePoint will remember the  user. PirateEric outlined a possible solution: Use a SharePoint list to save the user name when the button is clicked. When the page loads, look up the user in the list. If they already exist, redirect the page, if not, show the splash page with the button to accept the policy. If the policy changes and users need to be made aware of that, simply remove all items in the list that tracks the users. All this can be done with jQuery and web services. That intrigued me and I had a go at actually building this, using Marc Anderson’s SPServices.

How to set it up

Create a SharePoint custom list with two fields, Title and UserName. The former is the out of the box field, the latter is a simple text field. Create two pages, the Splash page with the button and the page that is the desired destination page for all visitors. In my sample these are called Splash.aspx and MainContent.aspx On the Splash page the code that you can see below will be loaded before any other web part. If you use a Content Editor Web Part to load the code with a content link, make sure that it’s the first web part on the page. In many cases, JavaScript and jQuery will be placed in the last web part of the page and run after the DOM has loaded. But in this case this would mean that the Splash page appears briefly, even if it is followed by a redirect to a different page. The Splash page provides the policy (or terms and conditions) that the user must accept, and a link or a button that the user can click to accept. This link or button must then trigger a JavaScript function. That is very easy to do. I used a button and put the html straight into a CEWP like this:

<button onclick="PolicyButtonClick()" type="submit">
   I accept the policy
</button>

So the user clicks the button and the JavaScript function PolicyButtonClick() will run. This function can be found in the code below. First, the jQuery library and the SPServices library are loaded. Then the user name of the current user is retrieved with the SPServices call using SPGetCurrentUser. It returns a text string in the format DOMAIN\Account.  Next, the SPServices call uses the GetListItem. In the call, a CAML query is constructed that will return only list items where the column UserName equals the current user.  The items returned by that query are then counted. Since the user account is a unique value, we can safely assume that the query will either return one result or no result at all. If the query returned an item, this means that the user has previously accepted the policy and the script will redirect to the MainContent.aspx page.  If the query did not return anything, the current page will continue to be displayed. When the user clicks the button to accept the policy,  the user name is written into a variable. Then the SPServices operation to UpdateListItem is called and will create a new item in the list “PolicyAccepted”, storing the previously established account name in the column UserName. Then the MainContent.aspx page is loaded. The next time the user opens the Splash page, their account name will be found in the PolicyAccepted list and they will not see the Splash page again, unless the entry in the list is deleted. Here is the complete script:

<script type="text/javascript" src="/path/jquery-1.10.2.min.js" language="javascript"></script><script type="text/javascript" src="/path/jquery.SPServices-2013.02a.min.js" language="javascript"></script><script type="text/javascript">// <![CDATA[
// start the code even before the DOM is loaded, so not waiting for document ready
//$(document).ready( function() {
// get the user name
 var userName= getUserName();
// find the user name in the list
 var userAccepted = matchUserName(userName);
 if (userAccepted == 1 )
 {
 // redirecting page
 window.location.replace("http://path/Documents/MainContent.aspx");
 }
//});

function getUserName() {
 var thisUserAccount= $().SPServices.SPGetCurrentUser({
 fieldName: "Name",
 debug: false
 });
 return(thisUserAccount);
}

function createNewItem(theTitle, theUser) {
 $().SPServices({
 operation: "UpdateListItems",
 async: false,
 batchCmd: "New",
 listName: "PolicyAccepted",
 valuepairs: [["Title", theTitle], ["UserName", theUser]],
 completefunc: function(xData, Status) {
 }
 });
}

function matchUserName(userName) {
 var queryText = "<Query><Where><Eq><FieldRef Name='UserName'/><Value Type='Text'>" + userName + "</Value></Eq></Where></Query>";
 $().SPServices({
 operation: "GetListItems",
 listName: "PolicyAccepted",
 async: false,
 CAMLQuery: queryText,
 completefunc: function (xData, status) {
 itemCount = $(xData.responseXML.xml).find("rs\\:data, data").attr("ItemCount");
 }
 });
 return(itemCount);
}

function PolicyButtonClick() {
 var userName= getUserName();
 var theTitle= "Accepted";
 createNewItem(theTitle, userName);
 window.location.href = "http://path/Documents/MainContent.aspx";
}
// ]]></script>

The MOSS Show SharePoint Podcast: Episode 87 – Talking JavaScript with Marc Anderson

The MOSS ShowBack in late January, I sat down – in the virtual sense, as usual – with Hilton Giesenow (@hiltongiesenow or the slightly more active @TheMossShow) to have a chat about JavaScript for his MOSS Show SharePoint Podcast. At the time, we were in the throes of trying to figure out what had happened on Office365 with some breaking DOM changes. Breaking for us JavaScript folks, anyway. We covered a number of topics like improving the user experience in SharePoint, what’s been going on with JavaScript over the years in the SharePoint community, and more.

From Hilton’s intro to Episode 87 – Talking JavaScript with Marc Anderson:

The entire web development world seems to have had a love-hate-love-hate relationship with JavaScript over the many years of its existence, and it’s no different in the SharePoint world, albeit a few years behind in many cases. In this episode we speak with many-year industry veteran and long time SharePointer Marc Anderson about his experiences with JavaScript and SharePoint over the years, his wildly successful SPServices library, Single-Page Applications (SPAs) and lots more.

Head on over to Episode 87 – Talking JavaScript with Marc Anderson and have a listen.

2014-03-18_22-53-36

KnockoutJS with SharePoint: Fixing ‘Error: You cannot apply bindings multiple times to the same element.’

KnockoutJS LogoI’ve been using KnockoutJS to build functionality into SharePoint pages for a while now. (In case you’ve ever wondered, yes, it’s the same thing people mean when they simply say “Knockout” or “KO”.)

Setting everything up with KnockoutJS can take a bit longer than hard-wiring things with jQuery – depending on what you are trying to accomplish and what your background may be – but once it’s all in place, changes to functionality become much easier. Some people naturally think in the Model-View-View Model (MVVM) pattern, but it took some time for me to get my head around the idea. Early on it felt like not just a separation of church and state, but also separation of the apse from the nave and Arkansas from middle, lower Arkansas.

I’ve probably run into every newbie error in the book, and this one has gotten me multiple times.

Error: You cannot apply bindings multiple times to the same element.

Error: You cannot apply bindings multiple times to the same element.

The screen snippet above is from my favorite debugger, Firebug. The error message itself doesn’t give you much to go on. It makes basic sense, since multiple bindings on the same element would seem to be a bad idea, but it doesn’t tell you anything about why it’s happening or what you’ve done to trigger the error. It just kills the page load.

Let’s take a simple example. In one of the sets of pages I’m working on, we want to have a header section driven by one view model and a body section driven by another. The two bindings are in separate .js files because the header logic is reused across pages, while the body section is unique to individual pages. (We’re using SharePoint 2010 more as a back-end repository and identity management provider in this application and replacing virtually all the UI with custom pages.) Each of the .js files performs the binding for which it is responsible.

The markup – at a high level – looks something like this:

<div id="abc-page-header">
...
</div>
<div id="abc-page-body">
...
</div>

and the bindings occur in the JavaScript looking something like this:

ko.applyBindings(new HeaderViewModel(), document.getElementById("abc-page-header"));
...
ko.applyBindings(new BodyViewModel(), document.getElementById("abc-page-body"));

As you can see, each binding is scoped to the container for which it has responsibility. Using document.getElementById is straighforward: we should get a JavaScript object reference to the containing element with that id.

All well and good, right? The problem in this case comes in when you have no second argument, or even worse, a typo in the id. For instance, I ran myself silly with document.getElementById("ab-page-body"). Because the document.getElementById function returned null (the id wasn’t in the page), the binding occurred on the entire document, and therein arises the problem. Elements with data-bind now become bound twice, throwing the error.

It would be much better if the error told us a little more about what was happening, of course. For instance, the error might mention which binding is currently applied to the particular element where the issue arises. Even better would be some sort of warning when you attempt to apply bindings with a null parameter. Alas, this is not the case.

The simple fix is to find the place in your code where a ko.applyBindings is applied incorrectly. It’s not so hard if you know that’s the right thing to do. If you don’t, you may end up tearing down your code and starting afresh, which is rarely productive.

See Creating view models with observables for the [rather sparse] documentation on ko.applyBindings. Note that the second parameter must be a JavaScript DOM node, not a jQuery object. If you try to get fancy like this:

ko.applyBindings(new HeaderViewModel(), $("#abc-page-header"));

You’ll get a very clear error:

rror: ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node

Error: ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node

Moral of the story: always scope your bindings carefully!

jQuery Library for SharePoint Web Services (SPServices) 2014.01 Released

SPServicesHot on the heels of the short-lived SPServices 2013.02 and it’s younger, wiser sibling 2013.02a, comes SPServices 2014.01. I wanted to do a new release with some cool new functionality, but Microsoft really forced my hand with some changes to title attributes on some column types. (See: Office 365 Update Changes ‘Display Name’ on Required Fields)

I recommend jQuery 1.10.x with this release. Even if you are a happy user of an older release of SPServices, I’d recommend upgrading to this release.

The headlines for this release:

  • Fix for the ” Required Field” change to column titles noted above on Office365 buildversion 16.0.2510.1204 (or thereabouts) and above and SharePoint 2010 with the December 2013 CU
  • New function SPGetListItemsJson
  • Exposed existing private function DropdownCtl as public function SPDropdownCtl
  • New objectType for SPXmlToJson: JSON
  • Case insensitive query string values with $().SPServices.SPGetQueryString
  • Added ClaimRelease operation to the Workflow Web Service

The SPGetListItemsJson function is pretty cool, if I do say so myself. When I fixed the operation GetListItemChangesSinceToken in 2013.02, I saw the possibility for an auto-conversion of the returned data to JSON with only one SOAP call. Because GetListItemChangesSinceToken returns both the list schema and the data, I’m able to parse the schema and return the data as JSON using SPXmlToJson. You can also pass in your own mappingOverrides if you need to.

GetListItemChangesSinceToken is also cool because it allows you to retrieve content deltas by passing in the old changeToken, and SPGetListItemsJson returns the changeToken and also accepts it for future calls. This is great support for the types of Single Page Applications I discuss in my ongoing blog series Single-Page Applications (SPAs) in SharePoint Using SPServices.

While I was building SPGetListItemsJson, I was working on a client project where we stored JSON as text in a Multiple lines of text column. To facilitate conversion of that data to internal JSON, I added an objectType to SPXmlToJson of “JSON”. This allows us to request automatic conversion of text-based JSON data to internally represented data simply by specify the JSON objectType in a mappingOverride. (The function uses jQuery’s $.parseJSON.)

With the kerfuffle about the Office 365 Update Changes ‘Display Name’ on Required Fields and the attendant fixes I needed to do in SPServices, I decided to make the SPDropdownCtl function I’ve been using for years public.  It’s not for the faint of heart, as it passes back a variable object depending on the type of “dropdown”, but it should help some people in building their own applications.

As usual, there are also a number of additional changes to fix existing bugs or improve efficiency (yes, I’m still able to improve on my old code, and I expect that will continue).

You can see the full list of enhancements and improvements on the download page. Note the link to the Issue Tracker items for this release. For posterity, here are links to the release notes showing all the fixes and improvements from the Issue Tracker on Codeplex.

New Functionality

Alpha Issue Tracker Item Function Description
ALPHA1 10216 $().SPServices.SPGetListItemsJson New Function: SPGetListItemsJson
ALPHA1 10221 $().SPXmlToJson Add new objectType to SPXmlToJson for text columns containing JSON
ALPHA1 10218 $().SPXmlToJson Add sparse parameter to SPXmlToJson
ALPHA1 10195 $().SPServices.SPGetQueryString Case insensitive SPGetQueryString?
ALPHA3 10229 $().SPServices.SPDropdownCtl Make DropdownCtl Function Public
ALPHA4 10230 $().SPServices.SPDropdownCtl Return optHid input element in SPServices.SPDropdownCtl

New Operations

Alpha Web Service Operation Options MSDN Documentation Issue Tracker Item
ALPHA2 Workflow ClaimRelease item, taskId, listId, fClaim http://msdn.microsoft.com/en-us/library/workflow.workflow.claimreleasetask(v=office.12).aspx 10222

Bug Fixes and Efficiency

Alpha Issue Tracker Item Function Description
ALPHA1 10211 $().SPServices.SPGetQueryString LMenuBaseUrl is undefined
ALPHA1 10192 $().SPServices.SPGetQueryString DeleteWeb needs_SOAPAction Value Incorrect
ALPHA1-2 10219 $().SPServices.SPXmlToJson Various Fixes to SPXmlToJSON
ALPHA2 10224 $().SPServices.SPCascadeDropdowns ParentObject Null in $.fn.SPServices.SPCascadeDropdowns
ALPHA2 10225 $().SPServices.SPXmlToJson SPXmlToJson – Fix Date/Time Conversion
ALPHA2 10226 $().SPServices.SPCascadeDropdowns Cascade Dropdowns Do not work if Fields Are Required
ALPHA4 10231 $().SPServices.SPCascadeDropdowns Title Changed for Required Fields after CU for Sharepoint 2010
BETA2 10238 $().SPServices.SPDisplayRelatedInfo SPDisplayRelatedInfo not working in SPServices 2014.01
BETA2 10236 NA Replace .attr(“value”) usage with .val()
BETA2 10235 $().SPServices.SPRequireUnique SPRequireUnique bug getting entered value

Regex Selector for jQuery by James Padolsey FTW

In researching how to fix the issue with Office 365 Update Changes ‘Display Name’ on Required Fields in SPServices, I came across some true awesomeness from James Padolsky (@).

jQuery is eminently extendable, and people do it all the time by creating plugins, additional libraries, etc. After all, it’s all just JavaScript, right? However, I’ve never seen such a nicely packaged selector extension as what James has done with his :regex extension.

In his Regex Selector for jQuery, James has given us exactly what we need to get around this nasty ” Required Field” thing.

jQuery.expr[':'].regex = function(elem, index, match) {
  var matchParams = match[3].split(','),
    validLabels = /^(data|css):/,
    attr = {
      method: matchParams[0].match(validLabels) ?
        matchParams[0].split(':')[0] : 'attr',
        property: matchParams.shift().replace(validLabels,'')
    },
    regexFlags = 'ig',
    regex = new RegExp(matchParams.join('').replace(/^\s+|\s+$/g,''), regexFlags);
  return regex.test(jQuery(elem)[attr.method](attr.property));
}

By adding this nice little chunk of JavaScript into SPServices, I get a nice, clean way to implement a fix. In the snippet of code below from the DropDownCtl function in SPServices, I can maintain backward compatibility (all the way back to SharePoint 2007 – WSS 3.0, even)

// Simple, where the select's title attribute is colName (DisplayName)
//  Examples:
//      SP2013 <select title="Country" id="Country_d578ed64-2fa7-4c1e-8b41-9cc1d524fc28_$LookupField">
//      SP2010: <SELECT name=ctl00$m$g_d10479d7_6965_4da0_b162_510bbbc58a7f$ctl00$ctl05$ctl01$ctl00$ctl00$ctl04$ctl00$Lookup title=Country id=ctl00_m_g_d10479d7_6965_4da0_b162_510bbbc58a7f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_Lookup>
//      SP2007: <select name="ctl00$m$g_e845e690_00da_428f_afbd_fbe804787763$ctl00$ctl04$ctl04$ctl00$ctl00$ctl04$ctl00$Lookup" Title="Country" id="ctl00_m_g_e845e690_00da_428f_afbd_fbe804787763_ctl00_ctl04_ctl04_ctl00_ctl00_ctl04_ctl00_Lookup">
if ((this.Obj = $("select[Title='" + colName + "']")).length === 1) {
  this.Type = "S";
// Simple, where the select's id begins with colStaticName (StaticName) - needed for required columns where title="colName Required Field"
//   Examples:
//      SP2013 <select title="Region Required Field" id="Region_59566f6f-1c3b-4efb-9b7b-6dbc35fe3b0a_$LookupField" showrelatedselected="3">
} else if ((this.Obj = $("select:regex(id, (" + colStaticName + ")(_)[0-9a-fA-F]{8}(-))")).length === 1) {
  this.Type = "S";

The regex I need to use isn’t too complicated, and the best part is that I can use it right in a jQuery selector with the :regex extension. Here I’m looking for an id which starts with the column’s StaticName followed by an underscore (_) and then 8 hexadecimal characters [0-9a-fA-F] and then a dash (-). That ensures that I’m finding the right id without the greedy selection issues on the title I may run into using some of the other methods people have suggested.

This fix is in the latest alpha of 2014.01, which is named 2014.01ALPHA2. I think that this will be the winning fix, and I’ve already gotten some positive feedback from folks who have tested it. If you have the issue and could test as well, I’d appreciate it. Because the DropDownCtl is the one function I use to find dropdowns for all of the other SPServices function, this should fix all of them.

Now, if we could just get the SharePoint Product Group to stop messing with our markup!