Second Life of a Hungarian SharePoint Geek

February 11, 2015

Migrating AngularJS-based SharePoint 2013 Applications to Former SharePoint Versions

Filed under: AngularJS, JSCOM, SP 2010, SP 2013 — Tags: , , , — Peter Holpar @ 23:34

In the past years we created several more or less simple AngularJS-based applications for SharePoint 2013 using REST / OData or the JavaScript Client Object Model (JSCOM). As we found this combo quite powerful and effective, we tried to apply the same technologies in other environments too, where we had to work with SharePoint 2010 (with and even without the visual upgrade). Since the migration into the backward direction was not quite trivial, I thought it might be worth to share my experience.

The sample application that I use in this post to illustrate the steps to fix the errors during the migration is really a simple one: it utilizes AngularJS v1.3.0-beta.6 to display the First Name, Last Name, Business Phone and Email Address fields of items from a standard SharePoint Contacts lists, and lets the items to be ordered as the user clicks on the field names in the header.

For example, we have a Contacts list like this:

image

that will be displayed as it shown below:

Contacts15

The HTML code that we used in a Content Editor web part to define the UI:

  1. <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script>
  2. <script type="text/javascript" src="/_layouts/15/sp.js"></script>
  3. <script type="text/javascript" src="/_layouts/15/contacts/angular.min.js"></script>
  4. <script type="text/javascript" src="/_layouts/15/contacts/contacts.js"></script>
  5.  
  6. <div ng-app="myApp">
  7.     <div ng-controller="contactsCtrl">
  8.         <table>
  9.             <tr>
  10.                 <td><a href="" ng-click="predicate = 'firstName'; reverse=!reverse">First name</a></td>
  11.                 <td><a href="" ng-click="predicate = 'lastName'; reverse=!reverse">Last name</a></td>
  12.                 <td><a href="" ng-click="predicate = 'eMail'; reverse=!reverse">E-Mail</a></td>
  13.                 <td><a href="" ng-click="predicate = 'workPhone'; reverse=!reverse">Phone</a></td>
  14.             </tr>
  15.                     <tr ng-repeat="contact in contacts | orderBy:predicate:reverse">
  16.                 <td>{{contact.firstName}}</td>
  17.                 <td>{{contact.lastName}}</td>
  18.                 <td><a href="mailto:{{contact.eMail}}">{{contact.eMail}}</a></td>
  19.                 <td>{{contact.workPhone}}</td>
  20.             </tr>
  21.         </table>
  22.     </div>
  23. </div>

The AngularJS controller (contactsCtrl) as well as the custom service (mySharePointService) were defined in the contacts.js. The service access the SharePoint list via JSCOM.

  1. 'use strict';
  2.  
  3. var contactListName = 'Contacts';
  4.  
  5. var myApp = angular.module('myApp', []);
  6.  
  7. myApp.service('mySharePointService', function ($q, $http) {
  8.  
  9.     this.getContacts = function ($scope) {
  10.         var deferred = $q.defer();
  11.  
  12.         var ctx = new SP.ClientContext.get_current();
  13.  
  14.         var web = ctx.get_web();
  15.         var list = web.get_lists().getByTitle(contactListName);
  16.  
  17.         var camlQuery = new SP.CamlQuery();
  18.         camlQuery.set_viewXml('<View><ViewFields><FieldRef Name=\'Title\'/><FieldRef Name=\'FirstName\'/><FieldRef Name=\'Email\'/><FieldRef Name=\'WorkPhone\'/></ViewFields></View>');
  19.         var contacts = list.getItems(camlQuery);
  20.  
  21.         ctx.load(contacts);
  22.  
  23.         ctx.executeQueryAsync(
  24.             function () {
  25.                 deferred.resolve(contacts);
  26.             },
  27.             function (sender, args) {
  28.                 deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  29.             }
  30.         );
  31.  
  32.         return deferred.promise;
  33.     };
  34.  
  35.  
  36. });
  37.  
  38. myApp.controller('contactsCtrl', function ($scope, mySharePointService) {
  39.     var promiseContacts = mySharePointService.getContacts($scope);
  40.  
  41.     promiseContacts.then(function (contacts) {
  42.         $scope.contacts = [];
  43.         var contactEnumerator = contacts.getEnumerator();
  44.         while (contactEnumerator.moveNext()) {
  45.             var contact = contactEnumerator.get_current();
  46.             $scope.contacts.push({
  47.                 firstName: contact.get_item('FirstName'),
  48.                 lastName: contact.get_item('Title'),
  49.                 workPhone: contact.get_item('WorkPhone'),
  50.                 eMail: contact.get_item('Email')
  51.             });
  52.         }
  53.  
  54.     }, function (errorMsg) {
  55.         console.log("Error: " + errorMsg);
  56.     });
  57. });

When we was to use the same HTML and .js in SP 2010 SP2 with the visual upgrade (that is the standard SP 2010 UI, below is a screenshot of the Contacts list itself), we had to face some issues:

ContactList14

First of all, although the data binding was correct and it has been initialized (the double curly braces disappeared), no data was displayed at all. When checking the console in the IE Developer Tools, we saw error messages like:

The collection has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.

Remark: In other cases, when we did not work with collections, but object properties, the error was:

The property or field has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.

As you may know, this error message typically indicates, that the collection / property has not been loaded into the client context. However, when I checked the network traffic between the client and server using Fiddler, I saw that the collection was requested by the client, and the response contains it as well. To make the symptoms even stranger, occasionally the contacts were displayed without any error message. This fact indicated me that it might be a kind of timing issue.

The solution to this issue was really inserting a delay (1 sec in our case) in the controller before we invoke the getContacts method of the mySharePointService service.

setTimeout(function () {
    $scope.getContacts($scope, mySharePointService);
}, 1000);

Second, sorting the items was not working anymore. Although one could see for a moment, that the sort order of the items was changed on clicking on the field name in the headers, but a second later the page reloaded and was displayed with the original sort order.

The solution to this problem was to replace the value of the href attribute of the anchors in the header. The original value was

<a href="" …

The new one, that changes the sort order without reloading the page:

<a href="javascript:;" …

We had a third error as well, that seems to be a consequence of the standard document mode (IE 8) used by the SharePoint 2010 master pages:

SCRIPT5014: Array.prototype.slice: ‘this’ is not a JavaScript object
angular.min.js, line 26 character 36

As we switched the document mode manually (for example, via the IE Developer Tools) the error disappeared. As this error had no negative effect on the functionality, we simply ignored that.

The updated version of the HTML content:

  1. <script type="text/javascript" src="/_layouts/sp.core.js"></script>
  2. <script type="text/javascript" src="/_layouts/sp.runtime.js"></script>
  3. <script type="text/javascript" src="/_layouts/sp.js"></script>
  4. <script type="text/javascript" src="/_layouts/contacts/angular.min.js"></script>
  5. <script type="text/javascript" src="/_layouts/contacts/contacts.js"></script>
  6.  
  7. <div ng-app="myApp">
  8. <div ng-controller="contactsCtrl">
  9.   <table>
  10.     <tr>
  11.         <td><a href="javascript:;" ng-click="predicate = 'firstName'; reverse=!reverse">First name</a></td>
  12.         <td><a href="javascript:;" ng-click="predicate = 'lastName'; reverse=!reverse">Last name</a></td>
  13.         <td><a href="javascript:;" ng-click="predicate = 'eMail'; reverse=!reverse">E-Mail</a></td>
  14.         <td><a href="javascript:;" ng-click="predicate = 'workPhone'; reverse=!reverse">Phone</a></td>
  15.     </tr>
  16.             <tr ng-repeat="contact in contacts | orderBy:predicate:reverse">
  17.         <td>{{contact.firstName}}</td>
  18.         <td>{{contact.lastName}}</td>
  19.         <td><a href="mailto:{{contact.eMail}}">{{contact.eMail}}</a></td>
  20.         <td>{{contact.workPhone}}</td>
  21.     </tr>
  22. </table>
  23. </div>
  24. </div>

The full and updated .js file:

  1. 'use strict';
  2.  
  3. var contactListName = 'Contacts';
  4.  
  5. var myApp = angular.module('myApp', []);
  6.  
  7. myApp.service('mySharePointService', function ($q, $http) {
  8.  
  9.   this.getContacts = function ($scope) {
  10.     var deferred = $q.defer();
  11.  
  12.     var ctx = new SP.ClientContext.get_current();
  13.  
  14.     var web = ctx.get_web();
  15.     var list = web.get_lists().getByTitle(contactListName);
  16.  
  17.     var camlQuery = new SP.CamlQuery();
  18.     camlQuery.set_viewXml('<View><ViewFields><FieldRef Name=\'Title\'/><FieldRef Name=\'FirstName\'/><FieldRef Name=\'Email\'/><FieldRef Name=\'WorkPhone\'/></ViewFields></View>');
  19.     var contacts = list.getItems(camlQuery);
  20.  
  21.     ctx.load(contacts);
  22.  
  23.     ctx.executeQueryAsync(
  24.             function () {
  25.               deferred.resolve(contacts);
  26.             },
  27.             function (sender, args) {
  28.               deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  29.             }
  30.         );
  31.  
  32.     return deferred.promise;
  33.   };
  34.  
  35. });
  36.  
  37. myApp.controller('contactsCtrl', function ($scope, mySharePointService) {
  38.     
  39.     setTimeout(function () {
  40.       $scope.getContacts($scope, mySharePointService);
  41.     }, 1000);
  42.  
  43.     $scope.getContacts = function ($scope, mySharePointService) {
  44.       var promiseContacts = mySharePointService.getContacts($scope);
  45.  
  46.       promiseContacts.then(function (contacts) {
  47.         $scope.contacts = [];
  48.         var contactEnumerator = contacts.getEnumerator();
  49.         while (contactEnumerator.moveNext()) {
  50.           var contact = contactEnumerator.get_current();
  51.           $scope.contacts.push({
  52.             firstName: contact.get_item('FirstName'),
  53.             lastName: contact.get_item('Title'),
  54.             workPhone: contact.get_item('WorkPhone'),
  55.             eMail: contact.get_item('Email')
  56.           });
  57.         }
  58.  
  59.       }, function (errorMsg) {
  60.         console.log("Error: " + errorMsg);
  61.       });
  62.     };
  63. });

The result as rendered by a Content Editor web part:

Contacts14

In case of SP 2010 SP2 without the visual update (that is the WSS 3.0 / MOSS 2007 UI, see the screenshot of the list below) we had all the above mentioned problems, and the following one:

ContactList12

The data binding was not working at all, see the image below:

NoDataBinding

It turned out to have two causes, both of them was an effect of the standard document mode (quirks mode) used by the WSS 3.0 master pages on working of AngularJS.

To solve these issues, first we had to append id="ng-app" to the HTML DIV we bind the AngularJS application to.

The original version was:

<div ng-app="myApp">

The new one is:

<div ng-app="myApp" id="ng-app">

Second, we had to disable the $sce service of AngularJS in the application if the document is displayed in quirks mode. We can achieve that via the config method of the application:

myApp.config(function ($sceProvider) {
    // Completely disable SCE to support IE7 (quirks mode in SharePoint 2007 / 2010).
    // SCRIPT5022: [$sce:iequirks]
http://errors.angularjs.org/1.3.0-beta.6/$sce/iequirks
    if ((document.documentMode == 5) || (document.documentMode == 7)) {
        // or: if (document.documentMode < 8) {
        $sceProvider.enabled(false);
    }
});

The updated version of the HTML content:

  1. <script type="text/javascript" src="/_layouts/sp.core.js"></script>
  2. <script type="text/javascript" src="/_layouts/sp.runtime.js"></script>
  3. <script type="text/javascript" src="/_layouts/sp.js"></script>
  4. <script type="text/javascript" src="/_layouts/contacts/angular.min.js"></script>
  5. <script type="text/javascript" src="/_layouts/contacts/contacts12.js"></script>
  6.  
  7. <div ng-app="myApp" id="ng-app">
  8. <div ng-controller="contactsCtrl">
  9.   <table>
  10.     <tr>
  11.         <td><a href="javascript:;" ng-click="predicate = 'firstName'; reverse=!reverse">First name</a></td>
  12.         <td><a href="javascript:;" ng-click="predicate = 'lastName'; reverse=!reverse">Last name</a></td>
  13.         <td><a href="javascript:;" ng-click="predicate = 'eMail'; reverse=!reverse">E-Mail</a></td>
  14.         <td><a href="javascript:;" ng-click="predicate = 'workPhone'; reverse=!reverse">Phone</a></td>
  15.     </tr>
  16.             <tr ng-repeat="contact in contacts | orderBy:predicate:reverse">
  17.         <td>{{contact.firstName}}</td>
  18.         <td>{{contact.lastName}}</td>
  19.         <td><a href="mailto:{{contact.eMail}}">{{contact.eMail}}</a></td>
  20.         <td>{{contact.workPhone}}</td>
  21.     </tr>
  22. </table>
  23. </div>
  24. </div>

The full and updated .js file:

  1. 'use strict';
  2.  
  3. var contactListName = 'Contacts';
  4.  
  5. var myApp = angular.module('myApp', []);
  6.  
  7. myApp.service('mySharePointService', function ($q, $http) {
  8.  
  9.   this.getContacts = function ($scope) {
  10.     var deferred = $q.defer();
  11.  
  12.     var ctx = new SP.ClientContext.get_current();
  13.  
  14.     var web = ctx.get_web();
  15.     var list = web.get_lists().getByTitle(contactListName);
  16.  
  17.     var camlQuery = new SP.CamlQuery();
  18.     camlQuery.set_viewXml('<View><ViewFields><FieldRef Name=\'Title\'/><FieldRef Name=\'FirstName\'/><FieldRef Name=\'Email\'/><FieldRef Name=\'WorkPhone\'/></ViewFields></View>');
  19.     var contacts = list.getItems(camlQuery);
  20.  
  21.     ctx.load(contacts);
  22.  
  23.     ctx.executeQueryAsync(
  24.             function () {
  25.               deferred.resolve(contacts);
  26.             },
  27.             function (sender, args) {
  28.               deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  29.             }
  30.         );
  31.  
  32.     return deferred.promise;
  33.   };
  34.  
  35. });
  36.  
  37. myApp.controller('contactsCtrl', function ($scope, mySharePointService) {
  38.     
  39.     setTimeout(function () {
  40.       $scope.getContacts($scope, mySharePointService);
  41.     }, 1000);
  42.  
  43.     $scope.getContacts = function ($scope, mySharePointService) {
  44.       var promiseContacts = mySharePointService.getContacts($scope);
  45.  
  46.       promiseContacts.then(function (contacts) {
  47.         $scope.contacts = [];
  48.         var contactEnumerator = contacts.getEnumerator();
  49.         while (contactEnumerator.moveNext()) {
  50.           var contact = contactEnumerator.get_current();
  51.           $scope.contacts.push({
  52.             firstName: contact.get_item('FirstName'),
  53.             lastName: contact.get_item('Title'),
  54.             workPhone: contact.get_item('WorkPhone'),
  55.             eMail: contact.get_item('Email')
  56.           });
  57.         }
  58.  
  59.       }, function (errorMsg) {
  60.         console.log("Error: " + errorMsg);
  61.       });
  62.     };
  63.   });
  64.  
  65.   myApp.config(function ($sceProvider) {
  66.     // Completely disable SCE to support IE7 (quirks mode in SharePoint 2007 / 2010).
  67.     // SCRIPT5022: [$sce:iequirks] http://errors.angularjs.org/1.3.0-beta.6/$sce/iequirks
  68.     if ((document.documentMode == 5) || (document.documentMode == 7)) {
  69.       // or: if (document.documentMode < 8) {
  70.       $sceProvider.enabled(false);
  71.     }
  72.   });

The result as rendered by a Content Editor web part:

Contacts12

I assume that this method should work even in case of WSS 3.0 / MOSS 2007, although if you really work with this SharePoint version, you have to obviously re-write the custom mySharePointService service to utilize the SharePoint web services (for example, via the SPServices library) instead of JSCOM.

Note: Another alternative approach to include AngularJS-based content in former SharePoint version is to use an IFRAME (for example, via Page Viewer Web Part) to display a simple HTML document, that you can store either in a document library in SharePoint or in the file system in the _layouts hive. Since in this page you don’t use the master page that enforced the document mode that caused the issues above, you can work without the restrictions or problems we faced when included the AngularJS content in the .aspx page itself.

I hope that the tips and hints in this post help all of you to utilize the really great features of AngularJS even if you have to work with former versions of SharePoint.

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: