In this article, we will learn about using AngularJS in SharePoint 2013. Specifically, we’ll see how we can do the CRUD operations on a list in the host web from a SharePoint hosted app using AngularJS.
First, create a SharePoint Hosted App in Visual Studio.
I am using a Books
list which I created during the demo of Workflows in SharePoint. You can refer to that article to create a similar list or you can modify the below code to suit your list information.
Download and add twitter bootstrap library bootstrap.min.css to the Content module in Visual Studio solution which will provide some starting style classes.
Reading from a List
Add the following references to the Default.aspx page – PlaceHolderAdditionalPageHead
section:
<script type="text/javascript" src="/_layouts/15/sp.RequestExecutor.js" ></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js">
</script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.js"></script>
<link rel="Stylesheet" type="text/css" href="../Content/bootstrap.min.css" />
In the App.js, define the angular module. This is the root AngularJS element which contains all the other elements such as the controllers and services.
var demoApp = angular.module('demoApp', ['ngRoute']);
In the Default.aspx, remove the existing div
tag and add the following div
section. The div
is marked as ng-app
with name as demoApp
. This is the main AngularJS directive which marks the section as an Angular app. demoApp
which we specified with this directive should match with the name of the angular module we created.
Within that, there is another div
section which specifies controller with ng-controller
directive. We will create this controller in a separate JavaScript file.
<div class="container">
<div ng-app="demoApp">
<div ng-controller="BookController">
<div class="navbar">
<div class="navbar-inner">
<ul class="nav">
<li><a ng-href="./NewBook.aspx?
{{queryString}}">Add new book</a></li>
</ul>
</div>
</div>
<div>Books: {{books.length}}</div>
<div class="row" ng-repeat="book in books">
<div class="row">
<div class="span11">
<h2>{{book.title}}</h2>
</div>
</div>
<div class="row">
<div class="span3">
<div><strong>Book Author:</strong> {{book.author}}</div>
<div><strong>Book Category:</strong> {{book.category}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
We have included the NewBook.aspx in the custom navigation, but this link will not work now, as we are yet to add this page. We will add this page in the next section where we add a page to create a new list item.
In the Scripts
module, create two folders, controllers and services.
In the services folder, add a JavaScript file BookService.js and add the following code to it.
demoApp.factory('bookService', function ($q, $http) {
return{
getBooks: function () {
var deferred = $.Deferred();
JSRequest.EnsureSetup();
hostweburl = decodeURIComponent(JSRequest.QueryString["SPHostUrl"]);
appweburl = decodeURIComponent(JSRequest.QueryString["SPAppWebUrl"]);
var restQueryUrl = appweburl +
"/_api/SP.AppContextSite(@target)/web/lists/getByTitle('Books')/items?$select=Title,
ID,BookAuthor,BookCategory&@target='" + hostweburl + "'";
var executor = new SP.RequestExecutor(appweburl);
executor.executeAsync({
url: restQueryUrl,
method: "GET",
headers: { "Accept": "application/json; odata=verbose" },
success: function (data, textStatus, xhr) {
deferred.resolve(JSON.parse(data.body));
},
error: function (xhr, textStatus, errorThrown) {
deferred.reject(JSON.stringify(xhr));
}
});
return deferred;
}
}
})
In the above code, we have defined a service called bookService
. It contains a method getBooks
which makes the actual REST call to the Books
list present in the hostweb. It makes an asynchronous call and returns the promise or the deferred object immediately.
Once the asynchronous call is completed, if it is successful, it will contain the items from the Books
list else it will move to the error block which loads the error message.
Using services in AngularJS allows us to organize and reuse code throughout the app. Services are implemented using the Dependency Injection design pattern. Services are lazily instantiated, i.e., only when a component depends on it, it is loaded. And, they are singletons, i.e., only one instance is created and each component gets a reference to this instance.
In the controllers folder, add a JavaScript file BookController.js and add the following code to it. Here you can see, we have added a dependency to the bookService
. Also, notice we are adding the controller to the demoApp
angular module.
demoApp.controller('BookController', ['$scope', 'bookService',
function BookController($scope, bookService)
{
SP.SOD.executeOrDelayUntilScriptLoaded(SPLoaded, "SP.js");
function SPLoaded() {
$scope.books = [];
$.when(bookService.getBooks($scope))
.done(function (jsonObject) {
angular.forEach(jsonObject.d.results, function (book) {
$scope.books.push({
title: book.Title,
author: book.BookAuthor,
category: book.BookCategory,
id: book.ID
});
if (!$scope.$$phase) { $scope.$apply(); }
});
})
.fail(function (err) {
console.info(JSON.stringify(err));
});
}
}]);
Here, in the controller, we call the method getBooks
contained in the service. The promise received from the service has the done
and fail
methods.
If the async call is a success, the done
method is called. If it fails, the fail
method is called. Inside the done
method, we are looping through the books
collection and adding it to the books
array in $scope
.
In the Default.aspx html markup, we can access the data within the $scope
using {{ }}
syntax. ng-repeat
directive dynamically creates a div
for each book
in the books
collection.
Also in Default.aspx, add the following script references after the App.js script reference:
<script type="text/javascript" defer="defer"
src="../Scripts/services/BookService.js"></script>
<script type="text/javascript" defer="defer"
src="../Scripts/controllers/BookController.js"></script>
Since we are trying to access the list from host web, we need to explicitly request for permissions. To do this, open the AppManifest.xml and request for Lists -> Full control.
Now, if you deploy and load the app, you should get a trust message. Select Books
list and click on ‘Trust It’.
Now, you should be able to see a list of Book
s.
Creating a New List Item
Right click on the Pages module and add a Page
element with the name NewBook.aspx. Add the libraries and CSS references which you have added to Default.aspx to this page also. Ensure that the controller reference is changed to NewBookController.js.
<script type="text/javascript" defer="defer"
src="../Scripts/controllers/NewBookController.js"></script>
Right click on Scripts module – controllers folder, add a JavaScript file NewBookController.js and add the following code:
demoApp.controller('NewBookController', ['$scope', 'bookService',
function EditBookController($scope, bookService) {
$scope.queryString = document.URL.split('?')[1];
$scope.saveBook = function (book) {
bookService.saveBook(book)
.success(function (response) {
window.location = "./Default.aspx?" + document.URL.split('?')[1];
})
.error(function (data, status, headers, config) {
console.log('failure', data);
});
}
$scope.cancelEdit = function () {
window.location = "./Default.aspx" + document.URL.split('?')[1];
}
}]
)
and in the services folder, add a method saveBook
:
saveBook: function (book) {
JSRequest.EnsureSetup();
hostweburl = decodeURIComponent(JSRequest.QueryString["SPHostUrl"]);
appweburl = decodeURIComponent(JSRequest.QueryString["SPAppWebUrl"]);
var restQueryUrl = appweburl +
"/_api/SP.AppContextSite(@target)/web/lists/getByTitle('Books')/items?@target='" +
hostweburl + "'";
var digest = document.getElementById('__REQUESTDIGEST').value;
var item = {
Title: book.Title,
BookAuthor: book.BookAuthor,
BookCategory: book.BookCategory,
__metadata: { type: 'SP.Data.BooksListItem' }
};
var requestHeader = {
get: {
'headers': {
'accept': 'application/json;odata=verbose'
}
},
post: {
'headers': {
'X-RequestDigest': digest,
'content-type': 'application/json;odata=verbose',
'accept': 'application/json;odata=verbose'
}
}
};
return $http.post(restQueryUrl, item, requestHeader.post);
}
In the NewBook.aspx, remove the WebPartZone
tag which is added by default and replace it with the following markup:
<SharePoint:ScriptLink Name="sp.RequestExecutor.js" runat="server"
LoadAfterUI="true" Localizable="false" />
<div class="container">
<div ng-app="demoApp">
<div ng-controller="NewBookController">
<div class="container">
<div class="navbar">
<div class="navbar-inner">
<ul class="nav">
<li><a ng-href="./Default.aspx?
{{queryString}}">Add new book</a></li>
</ul>
</div>
</div>
<h1>New Book</h1>
<hr />
<form>
<fieldset>
<label for="title"></label>
<input required id="title"
type="text" ng-model="newBook.Title"
placeholder="Title of book.." />
<label for="bookAuthor"></label>
<input id="bookAuthor"
type="text" ng-model="newBook.BookAuthor"
placeholder="Author of book.." />
<label for="bookCategory"></label>
<input id="bookCategory"
type="text" ng-model="newBook.BookCategory"
placeholder="Category of book.." />
<br />
<br />
<button type="submit"
ng-click="saveBook(newBook)" class="btn btn-primary">Save
</button>
<button type="button"
ng-click="cancelEdit()" class="btn btn-default">Cancel
</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
Now if you run the app, and click on the ‘Add new book’ in the custom top nav we added, you should get a form to add a new item. After adding the data and clicking on save will create a new item.
Editing List Items
For editing list items, I found that using ng-repeat
was a bit cumbersome, especially to make only the particular row as edit mode. So, I tried the ui-grid
and it worked pretty well. In the Default.aspx, remove the ng-repeat
section and replace it with the below markup.
<div ui-grid="gridOptions"
ui-grid-edit ui-grid-selection class="myGrid"></div>
Also, add the cdn
reference to the library
and css
.
<script src="//cdn.rawgit.com/angular-ui/bower-ui-grid/master/ui-grid.min.js"></script>
<link rel="Stylesheet" type="text/css"
href="//cdn.rawgit.com/angular-ui/bower-ui-grid/master/ui-grid.min.css"></link>
For this to work, you need to add dependencies to ui.grid
, ui.grid.edit
and ui.grid.selection
. So, the angular module declaration becomes:
var demoApp = angular.module('demoApp',
['ngRoute','ui.grid', 'ui.grid.edit', 'ui.grid.selection']);
In the ui-grid
, we have specified gridOptions
which contains the data and configuration information. So, add it in the BookController.js as shown below.
Also notice in the gridOptions
, we have specified a cellTemplate
to display the Save button. On click of this, in order to call the edit method in the $scope
, we have to use grid.appScope.edit
because the grid itself is in a different scope and we need to call the external scope. The row.entity
will send the row information to the event handler.
$scope.gridOptions = {
data: $scope.books,
columnDefs: [
{ name: 'title', field: 'title' },
{ name: 'author', field: 'author' },
{ name: 'category', field: 'category' },
{ name: 'edit', cellTemplate:
'<button type="submit" class="btn btn-primary"
ng-click="grid.appScope.edit(row.entity)">Save</button>' }
]
};
$scope.edit = function (book) {
bookService.saveBook(book)
.success(function (response) {
alert('item updated successfully');
})
.error(function (data, status, headers, config) {
console.log('failure', data);
});
}
We are using the same saveBook
method in the service which we have used for new item creation. The REST call to update a list item is slightly different from the new item creation. So, we need to update the method to take care of this. In the BookService.js, make the following changes:
- Add a new
headers
variable called update
:
update: {
'headers': {
'X-RequestDigest': digest,
"IF-MATCH": '*',
"X-HTTP-Method": 'MERGE',
'content-type': 'application/json;odata=verbose',
'accept': 'application/json;odata=verbose'
}
}
- Modify the
$http.post
to handle updates by checking if the book has an id. If the book already has an id, that means that it is an existing item:
if (book.id) {
restQueryUrl = appweburl +
"/_api/SP.AppContextSite(@target)/web/lists/getByTitle('Books')/items
(" + book.id + ")?@target='" + hostweburl + "'";
item = {
Title: book.title,
BookAuthor: book.author,
BookCategory: book.category,
__metadata: { type: 'SP.Data.BooksListItem' }
};
return $http.post(restQueryUrl, item, requestHeader.update);
}
else {
return $http.post(restQueryUrl, item, requestHeader.post);
}
Deleting List Items
After adding the edit functionality, adding the delete functionality is straightforward. In the gridOptions
, add a delete button so the gridOptions
looks as shown below and also add the delete
method to the scope.
$scope.gridOptions = {
data: $scope.books,
columnDefs: [
{ name: 'title', field: 'title' },
{ name: 'author', field: 'author' },
{ name: 'category', field: 'category' },
{ name: 'edit', cellTemplate:
'<button type="submit" class="btn btn-primary"
ng-click="grid.appScope.edit(row.entity)">Save</button>' },
{ name: 'delete', cellTemplate:
'<button type="submit" class="btn btn-primary"
ng-click="grid.appScope.delete(row.entity.id)">Delete</button>' }
]
};
$scope.delete = function (id) {
bookService.deleteBook(id)
.success(function (response) {
console.log('success', data);
})
.error(function (data, status, headers, config) {
console.log('failure', data);
});
}
Finally, in the BookService.js, add the deleteBook
method which makes the actual REST call to delete the item.
deleteBook: function (id) {
var hostweburl = decodeURIComponent(JSRequest.QueryString["SPHostUrl"]);
var appweburl = decodeURIComponent(JSRequest.QueryString["SPAppWebUrl"]);
var digest = document.getElementById('__REQUESTDIGEST').value;
var requestHeader = {
delete: {
'headers': {
'X-RequestDigest': digest,
"IF-MATCH": '*',
"X-HTTP-Method": 'DELETE'
}
}
}
var restQueryUrl = appweburl +
"/_api/SP.AppContextSite(@target)/web/lists/getByTitle('Books')/items(" + id + ")?@target='" +
hostweburl + "'";
return $http.post(restQueryUrl, null, requestHeader.delete);
}
I have uploaded the complete source code for this application in github at SP-AngularApp.
In this article, we saw how to integrate AngularJS with SharePoint in a SharePoint Hosted App. We saw how to do all the CRUD operations on a list present in the host web. Hope you found this helpful. Let me know your thoughts in the comments below.
The post Using AngularJS in SharePoint appeared first on The SharePoint Guide.