Using AngularJS in SharePoint

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 sepecified 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&amp;@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
              });
              //update $scope
              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
App Permissions

Now if you deploy and load the app, you should get a trust message. Select Books list and click on ‘Trust It’.
Trust message

Now, you should be able to see a list of Books.

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')[email protected]='" + 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 + ")[email protected]='" + 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 + ")[email protected]='" + 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.

Get Free Email Updates!

Signup now and receive an email once I publish new content.

I will never give away, trade or sell your email address. You can unsubscribe at any time.

  • Egidio Caleiro Santoro

    reference guide!