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

Comments (13)

  1. Madhu RK says:

    Exactly what I was looking for, Thanks

  2. Thorsten Hans says:

    Hey Richard,

    nice to see another good blog post on Angular Apps for SharePoint. You should review #ShareCoffee. It can save a lot of plumbing that you've written manually in your app.

    Thorsten

  3. ethem says:

    It looks interesting.

    I tried to open the project in my machine installed VS2013, SharePoint 2013, it says "The target version of the SharePoint site is 15.0.4420.1017, which is lower than the minimum supported SharePoint version of the app. To fix this, go to the SharePoint tab on the app for SharePoint project’s properties page, and change the Target SharePoint Version property to the version of the SharePoint site or lower."

    how can I open this project ?  What do I need to do?

  4. NYDev says:

    Thanks Richard for the good tutorial.

    I created the same app in my sharepoint 2013 on Premise using claims with NTLM. Everything worked just fine. By when I moved the REST API code to connect REMOTELY to my SharePoint  2013 using javascript, it gives me a 401 error.

    Is there a way to authenticate Rest API with javascript remotely against SharePoint 2013 Claims with NTLM?  I'm creating a mobile app using javascript (Gordova) and just encountered this problem. I created native apps in iOS and Android against the same environment and calling REST API, and had no problems with NTLM. But when it comes to calling REST API from javscript REMOTELY against SharePoint with NTLM, I get the 401 access denied. The Cross-Origin problem is solved already, but not the 401.

    I looked everywhere in the internet, I saw many articles using office365, oAuth or using FBA authentication, but nothing on my problem.

    Please help me solve this problem, or give me some pointers.

  5. Josh says:

    Hey Richard.  Thank you for the time and effort you put into making these videos.  This is kind of a niche realm of Sharepoint development as the app you made in your example resides in the Sharepoint ecosystem, but is not really apart of it.  That is exactly where I am at now.

    I was wondering if you had a good technique for finding current user information?  This is pretty trivial when you have the ability to leverage the JS context, but not so much when you don't use the SP masterpage.  I know you can use something like /_api/web/getuserbyid(), but I would need to know the current user's ID for that to be effective.

    Thank you for time and any input you might have,  Again, great video.

  6. Andy M Norman says:

    One of the cleanest how to do presentation I have reviewed in a long time. Richard has taken a complex subject and dumbed it down so that most can easily follow along and hopefully be productive.

    Again, well done.

  7. Paul Fechtmeister says:

    This is an awesome introduction to Angular, thank you for this.  If there are other videos from you on SharePoint hosted custom apps or resources for next steps I would love to know.

    Once again, this is well done.

  8. Anoop says:

    Great stuff Richard. Thank you very much for this.

  9. Carlos Bueno says:

    See this Module for Sharepoint + AngularJs

    github.com/…/SennitShareAngularJs

  10. Jose Trinidad says:

    Hi, I couldn't open the project in VisualStudios Community 2013, does it require VisualStudio 2015? Thanks!

  11. Riishii says:

    I tried running your solution but I am facing below error while adding an item from the form:-

    WcfDataServices 5.6 is missing.

    Original error: System.InvalidOperationException: The required version of WcfDataServices is missing. Please refer to go.microsoft.com/fwlink for more information.     at Microsoft.SharePoint.Client.WcfDataServices56Info..ctor(ClientServiceHost serviceHost)     at Microsoft.SharePoint.Client.WcfDataServices56Info.GetInstance(ClientServiceHost serviceHost)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.ReadRequestODataObject(ServerStub serverStub, Object entity)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.InvokeMethodWithRequestODataEntry(Object target, ServerStub serverStub, String methodName)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.TryAddEntity(Object entity, ServerStub serverStub, Object& newEntity)     at Microsof… 4ca8279d-2331-e0c3-0d58-c7250bc627c4

    …t.SharePoint.Client.Rest.RestRequestProcessor.Process()     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.ProcessRequest() 4ca8279d-2331-e0c3-0d58-c7250bc627c4

    SocialRESTExceptionProcessingHandler.DoServerExceptionProcessing – SharePoint Server Exception [System.InvalidOperationException: The required version of WcfDataServices is missing. Please refer to go.microsoft.com/fwlink for more information.     at Microsoft.SharePoint.Client.WcfDataServices56Info..ctor(ClientServiceHost serviceHost)     at Microsoft.SharePoint.Client.WcfDataServices56Info.GetInstance(ClientServiceHost serviceHost)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.ReadRequestODataObject(ServerStub serverStub, Object entity)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.InvokeMethodWithRequestODataEntry(Object target, ServerStub serverStub, String methodName)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.TryAd… 4ca8279d-2331-e0c3-0d58-c7250bc627c4

    …dEntity(Object entity, ServerStub serverStub, Object& newEntity)     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.Process()     at Microsoft.SharePoint.Client.Rest.RestRequestProcessor.ProcessRequest()] 4ca8279d-2331-e0c3-0d58-c7250bc627c4

    serviceHost_RequestExecuted

    End CSOM Request. Duration=145 milliseconds.  

    I tried googling in but could not find anything related to this. Can you please help in resolving this issue?

  12. Bu says:

    Hey mr diZerega!

    Please take your time to answer some of the questions here.I try to open your solution on my Visual Studio 2015 but i cant.What do i have to do?

    When it comes to migrating the project to sharepoint you dont take the time to show things step by step you jump over many things,so i had to stop the video and try to copy some of the code visible, and i might have done mistakes so it doesnt work ,thats why i tried to open your solution.

  13. Nch says:

    Great post!

    I am using the exact solution provided by you for my on-prem SP farm. I have just made one change for the SharePoint version property in the Project Properties.

    It builds and installs perfectly fine. But for every operations, including the getArtists throws the exception "Error getting form digest value."

    Seems like the http request never completes successfully.

    Appreciate, if you can take a minute to respond to this issue.

    Thanks!

    Nch

Skip to main content