Second Life of a Hungarian SharePoint Geek

November 30, 2016

Creating an AngularJS Directive for Mouse Hold

Filed under: AngularJS — Tags: — Peter Holpar @ 21:12

Most of the time we use AngularJS to create our single-page application (SPA) in SharePoint and Project Server.

Recently we had a requirement that AngularJS does not provide an out-of-the-box solution for: there is a container on the page that displays a fix number of items and two buttons (one is located above the container, the other is below the container), that should scroll the items in the container back and forth. When the user clicks the upper button, new items should appear at the bottom of the container and the top items should disappear. When the user clicks the button below, the items formerly scrolled out at the top should re-appear and the items below should disappear. The user must be able to scroll the items one-by-one. However, there is a large number of items, so clicking the buttons 20 times just to scroll 20 items down would be rather inconvenient for the users.

So we need something similar, that we already have in the scroll bar in our traditional Windows applications:

  • If the user clicks on the arrow button at the bottom of the scroll bar, and holds the mouse button in the position (there is a single mouse down event but no mouse up event), the application will scroll the content in small steps down.
  • Similarly, if the user clicks on the arrow button at the top of the scroll bar, and holds the mouse button in the position (there is a single mouse down event but no mouse up event), the application will scroll the content in small steps up.
  • If the user clicks on either of the arrow buttons, holds the mouse button in this position, but moves the mouse pointer out of the area of the arrow button (the mouse down event is raised in the area of the button, but a mouse out event is raised before the mouse up event) the content will be scrolled only while the mouse pointer is over the arrow button.
  • If the user clicks on the screen as the mouse pointer is out of the arrow button area, holds the mouse button in this position, and moves the pointer over the arrow button only later (the mouse down event is raised outside the area of the button) the content will be not scrolled.

To sum up the above rules for our case: the single mouse down event should happen while the mouse pointer is over the button, and the action performed by the application (in our case it was scrolling up / down) is repeated until either a mouse up event or a mouse out event occurs.

We decided to implement the requirements as a reusable component in AngularJS, namely a directive, that encapsulates the functionality and enables to apply it to various HTML elements declaratively.

We also wanted to provide the following parameters to our component:

  • Which action the component should repeat, similar to the other, built-in AngularJS events, like ng-click. Its value should be the name of the JavaScript function available in the scope.
  • The time interval to configure the frequency of the repetition the action. Its value should be a numeric value of the delay in milliseconds.
  • The time interval to configure the delay for starting the repetition (for example, the user has to hold the mouse button down for 1 sec. to start the repetition, but once it is started, the action is performed in every 0.2 sec.) was in our first scope of work, but it was selected as victim of  feature cutting.

We found several similar solutions on web blogs and forums, but none of them fulfilled our demands completely, or they simply just didn’t work.

Our implementation was created as an attribute-level AngularJS directive: the mandatory ‘on-mouse-hold’ attribute contains the name of the function that would be invoked as action for the mouse hold event. The optionally ‘mouse-hold-repeat’ attribute contains the delay for the repetition (in milliseconds), the default value is 0.5 sec.

Note: In this post I illustrate the usage of the directive in a non-SharePoint-specific application for those of you who are not interested in SharePoint, and to separate this piece of functionality from the other (IMHO not less interesting) SharePoint-related stuff. I plan to write a further post about using the directive in a SharePoint-specific application, namely how to load items dynamically in case of scrolling using the JavaScript client object model.

The following HTML snippet illustrates using the directive in simple case. There are two buttons having different actions and delays. In this case we simply count a numeric value up and down.

  1. <div ng-app="myApp" ng-controller="counterCtrl">
  2.     <button type="button" on-mouse-hold="countUp">Count up</button>
  3.     <div>{{counter}}</div>
  4.     <button type="button" on-mouse-hold="countDown" mouse-hold-repeat="50">Count down</button>
  5. </div>

The functionality of the AngularJS directive is implemented in the JavaScript code below:

  1. 'use strict';
  2.  
  3. var myApp = angular.module('myApp', []);
  4.  
  5. myApp.controller('counterCtrl', function ($scope) {
  6.  
  7.     $scope.counter = 0;
  8.  
  9.     $scope.countDown = function () {
  10.         $scope.counter–;
  11.     }
  12.  
  13.     $scope.countUp = function () {
  14.         $scope.counter++;
  15.     }
  16.  
  17. }).directive('onMouseHold', function ($parse, $interval) {
  18.     var stop;
  19.  
  20.     var dirDefObj = {
  21.         restrict: 'A',
  22.         scope: { method: '&onMouseHold' },
  23.         link: function (scope, element, attrs) {
  24.             var expressionHandler = scope.method();
  25.             var actionInterval = (attrs.mouseHoldRepeat) ? attrs.mouseHoldRepeat : 500;
  26.  
  27.             var startAction = function () {
  28.                 expressionHandler();
  29.                 stop = $interval(function () {
  30.                     expressionHandler();
  31.                 }, actionInterval);
  32.             };
  33.  
  34.             var stopAction = function () {
  35.                 if (stop) {
  36.                     $interval.cancel(stop);
  37.                     stop = undefined;
  38.                 }
  39.             };
  40.  
  41.             element.bind('mousedown', startAction);
  42.             element.bind('mouseup', stopAction);
  43.             element.bind('mouseout', stopAction);
  44.         }
  45.     };
  46.  
  47.     return dirDefObj;
  48. });

If you want to test the functionality online, visit this page on jsfiddle.

Advertisements

October 19, 2015

Displaying Notifications and Status Messages from a SharePoint-based AngularJS Application Including a FormController

Assume the following requirements: We should create a Single Page Application (SPA) (no SharePoint App!) that reads data from SharePoint using the JavaScript client object model, allows the user to edit the values, displays if a field value was changed (if it is “dirty” vs. “pristine”), performs data validations (if it is “valid” vs. “invalid”) and lets the user to save the changes. AngularJS was selected as the presentation framework for the SPA. On data save, we should give the users feedback on the progress (like “Saving changes…”, “Save completed” or “Error during the save operation”) via standard SharePoint notifications and status messages.

Challenge 1:  The FormController of the AngularJS framework is based on the form HTML element. That means, if we would like to use the features of the FormController, like dirty / pristine, validation, etc., we should include a form element in our HTML application. However, our SharePoint page is an ASP.NET page, that already contains a form element, and it does not like to include multiple ones.

Solution 1: Although there are tricks to bypass this limitation (like this or this one), I chose another way to go. I’ve included a Page Viewer Web Part that displays a “pure” HTML page that is stored in a document library in SharePoint, as well as any other non-standard artifacts of the application (.js and .css files, etc.). This HTML page – displayed in an IFRAME by the Page Viewer Web Part – contains the form element, that does not interfere with the form element on the ASP.NET page.

You can display a notification by calling the SP.UI.Notify.addNotification method, similarly a status message is displayed via the SP.UI.Status.addStatus method. Both of these Notify and Status classes are defined in the SP.UI namespace in the sp.js (and its debug version in sp.debug.js). This JavaScript file is typically referenced in the standard SharePoint pages, however you should add a reference to it in your custom pages, like in the case of our HTML page. If you forget to add the reference, you will get an error like this one:

TypeError: Unable to get property ‘addNotification’ of undefined or null reference

Challenge 2:  There is no notification / status message displayed, even if you add the reference to the sp.js. The reason of the problem is, that the HTML elements required by these methods are defined in the master page of the standard SharePoint sites. Obviously, these elements are not found in our custom page in the IFRAME, so the messages are not displayed.

Solution 2: I’ve found two similar blog posts (this one and this one) describing a similar issue with IFRAME and notification messages in the case of Client App Parts. The first of this two posts states that the problem is the IFRAME itself, that prohibits the communication between the parent page and the IFRAME. Of course, that is wrong. The real reason is the different domain names in the URL of the app part (IFRAME) and the host page, as correctly stated in the second post. If we have the same domain name (and we do have in this case), we do not need the rather complex approach described by the posts(that is still valid for the Client App Parts).  Displaying a notification / status message from the script included in the HTML page in the IFRAME in our case is so simple as to prepend the text ‘parent.’ before the method invocation, for example:

var notifyId = parent.SP.UI.Notify.addNotification("Saving…", true);

Of course, in this case you are using the JavaScript and HTML objects on the parent page, so you don’t need to reference the sp.js in your HTML page.

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.

Blog at WordPress.com.