My current assignment is to build an application using AngularJS to manage a SharePoint 2013 site. There is a lot of information out there about the different versions of Angular, I have a list of good videos in my Twitter feed. But I had to cobble together a lot of different information to get my working version. I wanted to document it here, early in the process, in hopes someone else will benefit from it.

The current release is Angular4, but Angular1 or AngularJS (for JavaScript) as it is better known, is the only one that doesn't require additional deployed solutions, such as node.js. I needed a solution that is strictly script. There are several ways to talk to SharePoint from script, such as REST and CSOM. The REST sample I found works on my this web site, but not on the customer's network. So, CSOM it is, which is actually better as that is the preferred standard. I will list the code for reading from REST as well.

This tutorial requires a basic understanding of JavaScript, AngularJS, and the SharePoint object model, but I will cover the basics with a broad brush. Angular is based on the Model-View-Controller (MVC) pattern, where the Model contains the data, the Controller the code to manage the transactions, and the View provides the data to user. Angularis also is said to represent the Model-View-Whatever pattern, MVW or MV*, where the Controller becomes "Whatever', to accomplish the objective.

Although simple VM* examples were very straightfoward, pulling SharePoint list data was the major challenge I encountered. I found simple examples that worked, but customizing them proved to be more of a problem. For this example, I chose one of my simple SharePoint lists, Cigars, with a small number of fields:

Column
Type
ManufacturerSingle line of text
TitleSingle line of text
SizeSingle line of text
OriginSingle line of text
SourceSingle line of text
PriceSingle line of text
RatingNumber


Cigars.html


This is the  "home page" for the single-page application, there are several html miles that will be swapped in as needed. It is called a single-page application because only a portion of the page is reloaded, the user will not see flickering and the entire page is loaded only once.

Cigars.html.png

The output looks like this, when there are no hashes in the URL. Hashes can be handled by AngularJS to imitate subfolders and querystrings. For example, "/Cigars.aspx#/Display/5" can be handled to show the Display page for the Item with an ID of 5.

Cigars.png


Note the <div> tag, with the attribute ng-app. That tells the page to use the modCigars module. The <div> tag inside of it has the ng-view attribute. That tells the page that AngularJS will prvoide the content to put in that <div> placeholder. Not that other than the script file references, this is all we have on the first page. If there is other information we want to show on all pages (like links), we would add it above or below the main <div> tag.


Cigars.js


This file contains the modCigars module. The function tag is at the top and botom, the individual components will be discussed separately.

(function () {
  var app = angular.module('modCigars', ["modSPDataServices", "ngRoute"]);
}()); // end function

The Configuration component controls AngularJS behavior for the entire module. Here it uses the routeProvider class to determine which .html file is loaded. This gives us the single-page application capability.

//
// Configuration
//

  app.config( ["$locationProvider", "$routeProvider", function($locationProvider, $routeProvider) {
$locationProvider.hashPrefix(""); 

  $routeProvider
 
.when("/", {
templateUrl: "../SiteAssets/Cigars/List.html",
controller: "Cigars",
controllerAs: "vm"
})

.when("/Display/:ItemID", {
templateUrl: "../SiteAssets/Cigars/Display.html",
controller: "Cigar",
controllerAs: "vm"
})
.when("/Edit/:ItemID", {
templateUrl: "../SiteAssets/Cigars/Edit.html",
controller: "Cigar",
controllerAs: "vm"
})

.when("/New", {
templateUrl: "../SiteAssets/Cigars/Edit.html",
controller: "Cigar",
controllerAs: "vm"
})
.otherwise( {
template: "<h2>This is not a valid selection.</h2>"
})
  }]); // end app.config( ["$locationProvider", "$routeProvider", function($locationProvider, $routeProvider) {$locationProvider.hashPrefix(""); 
 
The .when command examines the hash portion of the URL and assigns a URL and controller accordingly. Thus, when the URL ends in "/" (no hashes), the List.html file is displayed within the placeholder on Cigars.html:

<div ng-app="modCigars">
<div ng-view=""></div>
</div>

The Cigar Controller is used by Display.html and Edit.html to retrieve a single List Item from the Cigars list and display it on the page. Edit.html is used without an Item ID to create a new List Item.

//
// Cigar Controller
//

app.controller("Cigar", ["$q", "$log", "$routeParams", "SPDataServices", 
function ($q, $log, $routeParams, SPDataServices) {

var vm = this;
var listItemID = $routeParams.ItemID;
var Controls = [vm.Manufacturer, vm.Title, vm.Size, vm.Origin, vm.Source, vm.Price, vm.Rating];
vm.fields = ["ID", "Manufacturer", "Title", "Size", "Origin", "Source", "Price", "Rating"];
var savedFields = ["Manufacturer", "Size", "Origin", "Source", "Price", "Rating"];
if (listItemID) {
SPDataServices.getListItem("Cigars", listItemID, vm.fields)
.then( function(result) {
vm.Manufacturer = result.get_item("Manufacturer")
vm.Title = result.get_item("Title")
vm.Size = result.get_item("Size")
vm.Origin = result.get_item("Origin")
vm.Source = result.get_item("Source")
vm.Price = result.get_item("Price")
vm.Rating = result.get_item("Rating")
})
.catch( function(failure) {
vm.message = failure;
console.log(failure);
});
} else {
var txtTitle = document.getElementById("txtTitle")
if (txtTitle) {
txtTitle.focus();
}
}
vm.editButtonClick = function () {
window.location.href = "#Edit/" + listItemID;
}

vm.closeButtonClick = function () {
window.location.href = "#";
}
 
vm.saveButtonClick = function () {
vm.Size = new Date(vm.Size).toISOString();
vm.Origin= new Date(vm.Origin).toISOString();
if (window.location.hash == "#/New") {
SPDataServices.addListItem("Cigars", savedFields,
[vm.Manufacturer, vm.Size, vm.Origin, vm.Source, vm.Price, vm.Rating])
.then(function(result) {
window.location.href = "#";
});
} else {
SPDataServices.saveListItem("Cigars", listItemID, savedFields,
[vm.Manufacturer, vm.Size, vm.Origin, vm.Source, vm.Price, vm.Rating])
.then(function(result) {
window.location.href = "#";
});
}
}

vm.cancelButtonClick = function () {
window.location.href = "#";
}  
}]); // end app.controller("Cigar", ["$q", "$log", "$routeParams", "SPDataServices", function ($q, $log, $routeParams, SPDataServices)
The Cigars Controller is used by List.html to retrieve a collection of List Items from the Cigars list and display it on the page.

//
// Cigars Controller
//
app.controller("Cigars", ['$q', "$log", "SPDataServices",
 function ($q, $log, SPDataServices) {
var vm = this;
console.log("Cigars?");
var queryString = "<View><Query>" +
 "<OrderBy><FieldRef Name='Title'/></OrderBy>" +
 "<Where>" +
   "<Eq><FieldRef Name='Rating'></FieldRef><Value Type='Number'>5</Value></Eq>" +
 "</Where>" +
 "</Query></View>";
var Controls = [vm.Manufacturer, vm.Title, vm.Size, vm.Origin, vm.Source, vm.Price, vm.Rating];
vm.fields = ["ID", "Manufacturer", "Title", "Size", "Origin", "Source", "Price", "Rating"];

SPDataServices.getListItems("Cigars", queryString, vm.fields)
.then(function(result) {
vm.cigars = result;
})
.catch(function(failure) {
vm.failure = failure;
});
vm.newButtonClick = function () {
window.location.href = "#New";
}
}]); // end app.controller("Cigars", ['$q', "$log", function ($q, $log) {

Display.html


This snippet shows a List Item in display mode.

Display.html.png

This is how it is displayed on the page.

Display.png


Edit.html

This snippet shows a List Item in edit mode.

Edit.html.png


This is how it is displayed on the page. A new List Item would have blank fields.

Edit.png


I realize these are simple examples, and I may modify these files later. But I wanted a simple example for instructional purposes. I detail the CSOM code to talk to SharePoint on this page: SPDataServices.