Getting started with Bootstrap and AngularJS (for the SharePoint Developer)

Over past few months I’ve traveled the world talking to developers about building applications with Office 365. One of my favorite topics is building apps with Bootstrap and AngularJS. I like this topic because it illustrates the “new” Microsoft and our commitment to open source technologies. Bootstrap and AngularJS are wildly popular frameworks and we want to enable developers to use these and just about any other framework/library with Office 365. For some traditional SharePoint/Office developers, these technologies can be unfamiliar given they were difficult/impossible to leverage in the past.

In this video I’ll illustrate the basics of Bootstrap and AngularJS from the ground up. I’ll start with a completely blank page and build it into a completed app in easy to follow steps. Finally, I’ll import the HTML/JS/CSS assets into a SharePoint app in Visual Studio and connect everything with REST. This is a beginner’s guide to Bootstrap and AngularJS focused on traditional SharePoint developers.

[View:https://www.youtube.com/watch?v=LXfVHrdqj7I]

Request Digest and Bootstrap

Two-thirds of the video is a pure beginner’s guide to Bootstrap and AngularJS with no mention of SharePoint/Office. However, the end discusses specific considerations of leveraging these technologies in a SharePoint app. Specifically, the challenge of Bootstrap and the Request Digest value for a page.

The SharePoint masterpage adds a hidden “__RequestDigest” input field to all SharePoint pages. The value of this input field must be included in the header of HTTP POSTS against the SharePoint REST endpoints (read: add/update/delete). However, if we are building an app that leverages Bootstrap, it is unlikely we will leverage the SharePoint masterpage for app pages. This means the “__RequestDigest” field will not exist on app pages. Instead, we can make an explicit REST call to get the Request Digest value when our app loads. I’ve created an ensureFormDigest function that I always add to my AngularJS service(s). I can wrap all my SharePoint operations in this function to ensure I have a Request Digest value available for POSTS.

Angular App/Service for SharePoint and ensureFormDigest

var app = angular.module('artistApp', ['ngRoute']).config(function ($routeProvider) {    $routeProvider.when('/artists', {        templateUrl: 'views/view-list.html',        controller: 'listController'    }).when('/artists/add', {        templateUrl: 'views/view-detail.html',        controller: 'addController'    }).when('/artists/:index', {        templateUrl: 'views/view-detail.html',        controller: 'editController'    }).otherwise({        redirectTo: '/artists'    });}); app.factory('shptService', ['$rootScope', '$http',  function ($rootScope, $http) {      var shptService = {};       //utility function to get parameter from query string      shptService.getQueryStringParameter = function (urlParameterKey) {          var params = document.URL.split('?')[1].split('&');          var strParams = '';          for (var i = 0; i < params.length; i = i + 1) {              var singleParam = params[i].split('=');              if (singleParam[0] == urlParameterKey)                  return singleParam[1];          }      }      shptService.appWebUrl = decodeURIComponent(shptService.getQueryStringParameter('SPAppWebUrl')).split('#')[0];      shptService.hostWebUrl = decodeURIComponent(shptService.getQueryStringParameter('SPHostUrl')).split('#')[0];       //form digest opertions since we aren't using SharePoint MasterPage      var formDigest = null;      shptService.ensureFormDigest = function (callback) {          if (formDigest != null)              callback(formDigest);          else {              $http.post(shptService.appWebUrl + '/_api/contextinfo?$select=FormDigestValue', {}, {                  headers: {                      'Accept': 'application/json; odata=verbose',                      'Content-Type': 'application/json; odata=verbose'                  }              }).success(function (d) {                  formDigest = d.d.GetContextWebInformation.FormDigestValue;                  callback(formDigest);              }).error(function (er) {                  alert('Error getting form digest value');              });          }      };       //artist operations      var artists = null;      shptService.getArtists = function (callback) {          //check if we already have artists          if (artists != null)              callback(artists);          else {              //ensure form digest              shptService.ensureFormDigest(function (fDigest) {                  //perform GET for all artists                  $http({                      method: 'GET',                      url: shptService.appWebUrl + '/_api/web/Lists/getbytitle(\'Artists\')/Items?select=Title,Genre,Rating',                      headers: {                          'Accept': 'application/json; odata=verbose'                      }                  }).success(function (d) {                      artists = [];                      $(d.d.results).each(function (i, e) {                          artists.push({                              id: e['Id'],                              artist: e['Title'],                              genre: e['Genre'],                              rating: e['AverageRating']                          });                      });                      callback(artists);                  }).error(function (er) {                      alert(er);                  });              });          }      };       //add artist      shptService.addArtist = function (artist, callback) {          //ensure form digest          shptService.ensureFormDigest(function (fDigest) {              $http.post(                  shptService.appWebUrl + '/_api/web/Lists/getbytitle(\'Artists\')/items',                  { 'Title': artist.artist, 'Genre': artist.genre, 'AverageRating': artist.rating },                  {                  headers: {                      'Accept': 'application/json; odata=verbose',                      'X-RequestDigest': fDigest                  }                  }).success(function (d) {                      artist.id = d.d.ID;                      artists.push(artist);                      callback();                  }).error(function (er) {                      alert(er);                  });          });      };       //update artist      shptService.updateArtist = function (artist, callback) {          //ensure form digest          shptService.ensureFormDigest(function (fDigest) {              $http.post(                  shptService.appWebUrl + '/_api/web/Lists/getbytitle(\'Artists\')/items(' + artist.id + ')',                  { 'Title': artist.artist, 'Genre': artist.genre, 'AverageRating': artist.rating },                  {                      headers: {                          'Accept': 'application/json; odata=verbose',                          'X-RequestDigest': fDigest,                          'X-HTTP-Method': 'MERGE',                          'IF-MATCH': '*'                      }                  }).success(function (d) {                      callback();                  }).error(function (er) {                      alert(er);                  });          });      };       //genre operations      var genres = null;      shptService.getGenres = function (callback) {          //check if we already have genres          if (genres != null)              callback(genres);          else {              //ensure form digest              shptService.ensureFormDigest(function (fDigest) {                  //perform GET for all genres                  $http({                      method: 'GET',                      url: shptService.appWebUrl + '/_api/web/Lists/getbytitle(\'Genres\')/Items?select=Title',                      headers: {                          'Accept': 'application/json; odata=verbose'                      }                  }).success(function (d) {                      genres = [];                      $(d.d.results).each(function (i, e) {                          genres.push({                              genre: e['Title']                          });                      });                      callback(genres)                  }).error(function (er) {                      alert(er);                  });              });          }      };       return shptService;  }]);

 

I do this so frequently, that I’ve created a Visual Studio code snippet for getting app web/host web URLs and ensureFormDigest. You can download it at the bottom of this post.

Conclusion

Hopefully you can see how Bootstrap and AngularJS can greatly accelerate web development. In the past these were difficult to use in conjunction with SharePoint/Office development, but not with the app model. Hopefully this video helped you get a better understanding of Bootstrap, AngularJS, and how they can work with SharePoint/Office. You can download the completed solution and code snippet below.

ArtistCatalog Solution

Angular Service Code Snippet