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.
Comments
Anonymous
February 24, 2015
Exactly what I was looking for, ThanksAnonymous
February 24, 2015
The comment has been removedAnonymous
February 25, 2015
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?Anonymous
February 26, 2015
The comment has been removedAnonymous
April 16, 2015
The comment has been removedAnonymous
May 12, 2015
The comment has been removedAnonymous
May 13, 2015
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.Anonymous
May 16, 2015
The comment has been removedAnonymous
July 11, 2015
See this Module for Sharepoint + AngularJs github.com/.../SennitShareAngularJsAnonymous
July 30, 2015
Hi, I couldn't open the project in VisualStudios Community 2013, does it require VisualStudio 2015? Thanks!Anonymous
August 25, 2015
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?Anonymous
February 16, 2016
The comment has been removedAnonymous
March 03, 2016
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