10.29.2015

Developing an Angular Web App inside of SharePoint: Part 1 - Hello World in SharePoint

Angular is currently my favorite JavaScript Library. It feels like a marriage of the best of Desktop Development with the best of Web Development. It paves the way to understanding mobile and responsive design. Data-binding makes managing data on your page much more simple than the event-driven style of jQuery. Routing makes lazy-loading the pages of your app quicker and it makes spinning up new pages for your app quicker and easier as well.

(I tend to be verbose in my writing. Look for "tl;dr;" for "too long, didn't read" summaries of sections.

The Sales Pitch on using Angular in SharePoint

If you are not already sold on the idea of drinking the Angular Kool-Aid, let me go into some reasons why you should. I have 5 years of server-side SharePoint development in 2007 and 2010 (less in 2013). When we had the ability to write farm solutions on the server, doing anything in JavaScript felt like peasant-work. We avoided it at all costs. When the idea of "single-page-apps" came along, we scoffed at it as unnecessary. When we heard that SP2013 would switch to a greater emphasis on client-side development (JavaScript) we cringed. JavaScript is terrible ... why would we want more of it?

Later, I switched to a contract where deploying anything to the server was more trouble than it was worth because of environmental and contracting issues. Out of necessity, I went down the JavaScript path and haven't turned back. When I came across Angular, I realized my decision was the correct one. SharePoint is slow and it is often deployed in environments with an equally slow intranet and on computers that should were already obsolete 2 years before they were purchased as employee workstations. Ever time you click a link in SharePoint, you load about 1-2 dozen JavaScript files, some aspx files, web parts, a few css files, etc. Each of these has to be downloaded (or checked against the cache) and loaded into memory and processed. Most of these files were the same ones you just had on the last page you were on!

Angular Fixes a lot of this problem with routing. When we go into routing, we will be trading out the default Angular-routing library with angular-ui-router, which is far superior. I'll go more into that later. Routing allows you to load up your entire Angular Web App, after which, you can go to different pages within your App (called views) WITHOUT reloading all those pesky JS files. This lazy-loading means that you're only loading a very small amount of data when you switch pages. You will also learn to use promises and asynchronous web service calls to load your pages before you have the data you need on the page, so the page will load quickly. The data will then follow onto the page a moment later, and using some subtle animation, the user will have no idea that he waited any time at all.

My last sales pitch will revolve around data-binding, which does not provide a specific advantage over SharePoint, but is just a really awesome and structured way of writing your code. Data-binding will make managing complex amounts of data far-easier than you are probably use to. If you've already used other data-binding frameworks like knockout then you will know what I'm talking about. Knockout is great, and I've used it, but the syntax in Angular is far friendlier. However, the friendliness of Angular's syntax comes at a cost: it may take a little longer for you to figure out how Angular works under the hood because so much is done for you.

Getting Started with a Hello World Example

At this point, I will assume that you are already sold on the idea of bringing the power of Angular to your SharePoint site. I'll bring up the advantages of doing so, less as an attempt to convince you of those advantages, and more to help you recognize when you are adding value for your customers when you write a site. I'll also assume that you have some experience with SharePoint, though I'll assume a very low amount of experience with Server-side development and Client Side Object Model (herein referred to as either CSOM or JSOM. You will here JSOM more often when there is a danger of confusing JavaScript CSOM with C# CSOM, and the two need to be distinguished). Lastly, Bootstrap will be referenced a lot at a basic level. This really just means that we will be using pre-defined bootstrap classes that work well with Angular.

To get started, we need to create a main page that contains the minimum that is required to hook into SharePoint and allow HTML5 on the page. Start by adding a folder in Site Assets, or really any library or location on your site. Keep everything you need for your application contained in a single folder that you can copy easily. This helps with branching, so you can have a dev branch you're working on, previous versions you've saved, and a production branch that users will ultimately use.

Next, add an aspx file (or any file type and rename it to .aspx). It is necessary for your home page to be aspx and not html because this allows you to add a key asp control that is used to interact with SharePoint. This allows SharePoint to know your username and provide a SharePoint Context Object. If you've done server side development then the bit about a SharePoint Context Object will make sense to you. If not, don't worry about it.

The code snippet below includes the minimum that is required to have a SharePoint-hosted HTML5 page that can access SharePoint resources as the current user. Credit goes to all that js, and is modified to include support for Angular and Bootstrap.

 
<!DOCTYPE html>
<%@ Page language="C#" %>
<%@ Register Tagprefix="SharePoint" 
     Namespace="Microsoft.SharePoint.WebControls" 
     Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<!--Example from: http://allthatjs.com/2012/04/03/using-sharepoint-csom-in-html5-apps/-->
<html>
<head>
<meta name="WebPartPageExpansion" content="full" />
 <!--I don't remember what each of these does exactly, but they were all necessary for one reason or another-->
 <meta http-equiv="X-UA-Compatible" content="IE=edge;chrome=1" />
 <meta name=GENERATOR content="Microsoft SharePoint">
 <meta http-equiv=Content-Type content="text/html; charset=utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1"> <!--Required for viewport commands in css. Responsive Design-->
 
 
  <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->   

    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">

    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0-beta.1/angular.min.js"></script>
</head>
<body ng-app="main-app" ng-controller="master-model">
    <SharePoint:FormDigest ID="FormDigest1" runat="server"></SharePoint:FormDigest>
    
    
    <!-- Begin Custom Section. Everything outside of this section should be included in any SP/Angular page that you start, minus any custom js/css I may have added -->
  <h3>{{data.message}}</h3>
 <p>{{data.site.title || 'Loading Site Details...'}}</p>
    <input id="description" class="form-control" type="text" ng-model="data.site.description" placeholder="Description">
    <p>{{data.user.username}} <a href="mailto:{{data.user.email}}">{{data.user.email}}</a></p>
   <button type="button" class="btn btn-default" ng-click="data.toggle = !data.toggle">Toggle Me!</button>
    <!-- Really good example of how you should be debugging -->
    <pre>{{data | json}}</pre>   
    <!-- End Custom Section -->
   
    
    <!-- Put all your js at the bottom so the page loads faster. Put angular at the top so it can start sinking its hooks earlier-->    

 <!-- the following 5 js files are required to use CSOM -->
    <script src="/_layouts/1033/init.js"></script>
    <script src="/_layouts/MicrosoftAjax.js"></script>
    <script src="/_layouts/sp.core.js"></script>
    <script src="/_layouts/sp.runtime.js"></script>
    <script src="/_layouts/sp.js"></script>
    
    <!--Using CDNs for demo purposes -->
    <script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0-beta.1/angular-animate.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.14.3/ui-bootstrap-tpls.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.14.3/ui-bootstrap.min.js"></script>
    <!-- include your app code --> 
    <script src="app.js"></script>
</body>
</html>

Next, set your app.js to the following:

 
(function(){
angular.module('main-app',['ngAnimate']).controller('master-model', ['$scope', '$q', function($scope, $q){
 function init(){
  $scope.data = { //Initialize data
   message: "Welcome to Angular!"
  }; 
  
  getSiteInformationUsingPromise().then(function(results){
   $scope.data.site = results.site;
   $scope.data.user = results.user;
  });


 } 
 
 function getSiteInformationUsingPromise(){
  return $q(function(resolve, reject){
   //Note that if you aren't using a master page, you can't just use $().SPServices.SPGetCurrentSite(). You have to use a relative path, not an absolute path which this function returns
   var ctx = new SP.ClientContext("/"); //This needs to include the entire relative path, included any site collections and subsites. This example uses the root site collection.
   var web = ctx.get_web();
   var user = web.get_currentUser();
   ctx.load(web);  
   ctx.load(user);
   ctx.executeQueryAsync(Function.createDelegate(this,
    function() {
     resolve({ //Angular knows to refresh the page after resolve is executed because we used an Angular Promise
      site: {
       title: web.get_title(),
       description: web.get_description()
      },
      user: {
       id: user.get_id(),
       username: user.get_title(),
       email: user.get_email()
      }
     })     
    }), Function.createDelegate(this, function(sender, args) {
     reject({
      title: 'Error'
     })
    }));
  });
 }

 
 init();
}]);
})();

There you go! This should be a basic Angular page with a minimal tie-in to SharePoint. In my next post I intend to expand further and eventually break the data-access layer out into a separate module as an Angular Factory. I'll also do some simple CRUD operations. In the mean time, look over the code and take note of many of the comments which drill down into a little more detail about why I'm doing things certain ways.