Thursday, August 27, 2015

SharePoint 2013 and Angularjs

SharePoint 2013 and Angularjs

Introduction

We all know that AngularJS is currently most famous for both way data binding framework among all. This article aims to show how this advantage (both way data binding) can be taken in SharePoint 2013. Before reading this article, you must have knowledge of AngularJS and SharePoint 2013 REST API.
AngularJS can help us in developing Content Editor Web Parts and apps. In most of the cases, I found that people choose Sandbox solution but the REST API of SharePoint 2013 is so powerful that Sandbox solutions can be replaced by the Content Editor Web Parts or apps easily. In this article, we will see how AngularJS can be used in Content Editor Web Parts and SharePoint hosted apps.

Initial Setup

For keeping everything simple, I will demonstrate a CRUD operation on a list because we know that CRUD is a very common part in any kinds of business requirement. I named the list as People. Apart from that, you will need SharePoint Designer 2013 and Visual Studio 2013 or 2012. Finally, make sure that your server is prepared for developing SharePoint hosted app.
people-list
The following features will be implemented such View All Items and Delete Item.
all

Add New Item

add

Edit Item

edit

AngularJs in Content Editor Web Part

If you are not familiar with Content Editor Web Part please have a look here. Now from Designer, create a folder in any asset library and then a text file. In my case, I have created a folder named People and text file namedpeople-main.txt inside Site Assets library. Finally, add this file URL in Content Link. My URL becomes/SiteAssets/People/people-main.txt as I was working on my root site collection. If you prefer to work from any sub-site, then add its path to the URL properly.
Now text file is ready for adding HTML code.
<link href="/SiteAssets/People/css/style.css" rel="stylesheet" type="text/css">
<script src="/SiteAssets/People/lib/angular.min.js" type="text/javascript"></script>
<script src="/SiteAssets/People/lib/angular-route.min.js" type="text/javascript"></script>
<script src="/SiteAssets/People/js/app.js" type="text/javascript"></script>
<script src="/SiteAssets/People/js/services/baseSvc.js" type="text/javascript"></script>
<script src="/SiteAssets/People/js/services/people/people.js" type="text/javascript"></script>
<script src="/SiteAssets/People/js/controllers/people/all.js" type="text/javascript"></script>
<script src="/SiteAssets/People/js/controllers/people/add.js" type="text/javascript"></script>
<script src="/SiteAssets/People/js/controllers/people/edit.js" type="text/javascript"></script>

<div data-ng-app="peopleApp">
    <div data-ng-view class="people-app"></div>
</div>
Actually, in this file, I have added the necessary script sources and initialised ng-app and ng-view. Rather than that, I have linked css file here also. According to the AngularJS project structure, Folder structure should become like the following:
folder-structure
Always be careful when adding the value of src and href of link and script tag. Now let's look at script files. Everything is very much similar as typical AngularJS apps. So I will explain the differences only.

app.js

"use strict";
(function() {
    angular.module("peopleApp", ["ngRoute"])
        .config(["$routeProvider", function($routeProvider) {
            $routeProvider.when("/", {
                templateUrl: "/SiteAssets/People/templates/people/all.html",
                controller: "allPeopleCtrl"
            }).when("/addPerson", {
                templateUrl: "/SiteAssets/People/templates/people/add.html",
                controller: "addPersonCtrl"
            }).when("/editPerson/:personId", {
                templateUrl: "/SiteAssets/People/templates/people/edit.html",
                controller: "editPersonCtrl"
            });
        }]);
})();
peopleApp is created here and necessary routes are defined. Again, be careful in templateUrl.

baseSvc.js

"use strict";
(function() {
    angular.module("peopleApp")
        .factory("baseSvc", ["$http", "$q", function($http, $q) {
            var baseUrl = _spPageContextInfo.webAbsoluteUrl;
            var getRequest = function(query) {
                var deferred = $q.defer();
                $http({
                        url: baseUrl + query,
                        method: "GET",
                        headers: {
                            "accept": "application/json;odata=verbose",
                            "content-Type": "application/json;odata=verbose"
                        }
                    })
                    .success(function(result) {
                        deferred.resolve(result);
                    })
                    .error(function(result, status) {
                        deferred.reject(status);
                    });
                return deferred.promise;
            };
            var postRequest = function(data, url) {
                var deferred = $q.defer();
                $http({
                        url: baseUrl + url,
                        method: "POST",
                        headers: {
                            "accept": "application/json;odata=verbose",
                            "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value,
                            "content-Type": "application/json;odata=verbose"
                        },
                        data: JSON.stringify(data)
                    })
                    .success(function(result) {
                        deferred.resolve(result);
                    })
                    .error(function(result, status) {
                        deferred.reject(status);
                    });
                return deferred.promise;
            };
            var updateRequest = function(data, url) {
                var deferred = $q.defer();
                $http({
                        url: baseUrl + url,
                        method: "PATCH",
                        headers: {
                            "accept": "application/json;odata=verbose",
                            "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value,
                            "content-Type": "application/json;odata=verbose",
                            "X-Http-Method": "PATCH",
                            "If-Match": "*"
                        },
                        data: JSON.stringify(data)
                    })
                    .success(function(result) {
                        deferred.resolve(result);
                    })
                    .error(function(result, status) {
                        deferred.reject(status);
                    });
                return deferred.promise;
            };
            var deleteRequest = function(url) {
                var deferred = $q.defer();
                $http({
                        url: baseUrl + url,
                        method: "DELETE",
                        headers: {
                            "accept": "application/json;odata=verbose",
                            "X-RequestDigest": document.getElementById("__REQUESTDIGEST").value,
                            "IF-MATCH": "*"
                        }
                    })
                    .success(function(result) {
                        deferred.resolve(result);
                    })
                    .error(function(result, status) {
                        deferred.reject(status);
                    });
                return deferred.promise;
            };
            return {
                getRequest: getRequest,
                postRequest: postRequest,
                updateRequest: updateRequest,
                deleteRequest: deleteRequest
            };
        }]);
})();
I make a generic service for all kinds of requests like GETPOSTUPDATE and DELETE so that we can inject it from anywhere. I am not describing here how these methods work because I have an another article about it. In that article, I demonstrated it using jQuery and in this I have just converted it into AngularJS. This baseSvc is injected in peopleService that benefitted us from repeating codes while getting, adding, updating and deleting items. Before sending any request, you have to pass required URL and parameters to the baseSvc methods.
"use strict";
(function() {
    angular.module("peopleApp")
        .factory("peopleService", ["baseSvc", function(baseService) {
            var listEndPoint = '/_api/web/lists';
            var getAll = function() {
                var query = listEndPoint + "/GetByTitle('People')/Items?$select=ID,
                         FirstName,LastName,Address";
                return baseService.getRequest(query);
            };
            var addNew = function(person) {
                var data = {
                    __metadata: {
                        'type': 'SP.Data.PeopleListItem'
                    },
                    FirstName: person.firstName,
                    LastName: person.lastName,
                    Address: person.address
                };
                var url = listEndPoint + "/GetByTitle('People')/Items";
                return baseService.postRequest(data, url);
            };
            var getById = function(personId) {
                var query = listEndPoint + "/GetByTitle('People')/GetItemById
                (" + personId + ")?$select=ID,FirstName,LastName,Address";
                return baseService.getRequest(query);
            };
            var update = function(person) {
                var data = {
                    __metadata: {
                        'type': 'SP.Data.PeopleListItem'
                    },
                    FirstName: person.firstName,
                    LastName: person.lastName,
                    Address: person.address
                };
                var url = listEndPoint + "/GetByTitle('People')/
                GetItemById(" + person.personId + ")";
                return baseService.updateRequest(data, url);
            };
            var remove = function(personId) {
                var url = listEndPoint + "/GetByTitle('People')/
                GetItemById(" + personId + ")";
                return baseService.deleteRequest(url);
            };
            return {
                getAll: getAll,
                addNew: addNew,
                getById: getById,
                update: update,
                remove: remove
            };
        }]);
})();
You may have a question that how did I construct the URLs and parameters for each request. Again, you have to follow my article.
Now look at templates and controllers.

All Items

This template is intended to show items and remove items.
<a href="#/addPerson" class="add-new-button">Add New Person</a>
<div class="all-people">
    <table>
        <tbody>
            <tr>
                <th>Fist Name</th>
                <th>Last Name</th>
                <th>Address</th>
                <th>Action</th>
            </tr>
            <tr data-ng-repeat="person in people">
                <td>{{person.FirstName}}</td>
                <td>{{person.LastName}}</td>
                <td>{{person.Address}}</td>
                <td>
                    <a href="#/editPerson/{{person.ID}}">
                       <img src="/SiteAssets/People/images/edit.png" alt=""></a>
                    <a href="" data-ng-click="removePerson(person)">
                       <img src="/SiteAssets/People/images/delete.png" alt=""></a>
                </td>
            </tr>
        </tbody>
    </table>
</div>
Respective controller for this view looks like the following:
"use strict";
(function () {
    angular.module("peopleApp")
        .controller("allPeopleCtrl", ["$scope", "peopleService",
        function ($scope, peopleService) {
            peopleService.getAll()
                .then(function (response) {
                $scope.people = response.d.results;
            });
            $scope.removePerson = function(person){
                peopleService.remove(person.ID)
                .then(function(response){
                    var personIndex = $scope.people.indexOf(person);
                    $scope.people.splice(personIndex,1);
                });
            };
        }]);
})();
You must notice that I have requested IDFirstNameLastName and Address in peopleService usinggetAll(). In this controller, items returned by peopleService are binded to the view using ng-repeat. Removing item is also implemented here. I am not describing other views and controllers here because it will elaborate this article unnecessarily. Hope you will understand everything after downloading my source code. So at first, deploy my code on your site, then explore it.
NB: Don't use any form tag inside the template.

AngularJS in Hosted App

Before developing hosted app for on-premises, environment must be configured properly. In that case, office-365 is much more easier. Environment configuration is not needed at all. Then, create a developer site collection by choosing Developer Site template. We cannot use any other sites except Developer Site for app development. Now add the same People list in this site also where we will do the same job like Content Editor.
Create a new project in Visual Studio and choose App for SharePoint 2013 from Office/SharePoint template.
New Project
Click ok, Enter your site URL which you want to use for debugging, choose SharePoint-Hosted and finally click finish.
Url selection
When we create a SharePoint Hosted app, it gives us a standard folder structure for our scripts and design files. We cannot follow what we did in Content Editor. I have added my files in the following structure.
app-folder
Basically, I have added my all scripts in Scripts folder, styles in Content folder and images to the Images folder. For template files, I have added a new folder named Templates. Now, open the Default.aspx page and modify it like below:
<%-- The following 4 lines are ASP.NET directives needed when using SharePoint components --%>
<%@ Page Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, 
Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" 
MasterPageFile="~masterurl/default.master" Language="C#" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" 
Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" 
Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" 
Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%-- The markup and script in the following Content element 
will be placed in the <head> of the page --%>
<asp:Content ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
    <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.js"></script>
    <meta name="WebPartPageExpansion" content="full" />
    <link rel="Stylesheet" type="text/css" href="../Content/style.css" />
    <script src="../Scripts/angular.min.js" type="text/javascript"></script>
    <script src="../Scripts/angular-route.min.js" type="text/javascript"></script>
    <script src="../Scripts/peopleApp/app.js" type="text/javascript"></script>
    <script src="../Scripts/peopleApp/services/baseSvc.js" type="text/javascript"></script>
    <script src="../Scripts/peopleApp/services/people/people.js" type="text/javascript"></script>
    <script src="../Scripts/peopleApp/controllers/people/all.js" type="text/javascript"></script>
    <script src="../Scripts/peopleApp/controllers/people/add.js" type="text/javascript"></script>
    <script src="../Scripts/peopleApp/controllers/people/edit.js" type="text/javascript"></script>
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
    SharePoint 2013 Hosted App and AngularJS Demo
</asp:Content>
<asp:Content ContentPlaceHolderID="PlaceHolderMain" runat="server">
    <div data-ng-app="peopleApp">
        <div data-ng-view class="people-app"></div>
    </div>
</asp:Content>
In this page, I have added all script and style references and also initialized peopleApp here insidePlaceHolderMain. Now there is a little change in app.js and baseSvc.js file. In app.js, we have to change the template URLs.
"use strict";
(function () {
    angular.module("peopleApp", ["ngRoute"])
        .config(["$routeProvider", function ($routeProvider) {
            $routeProvider.when("/", {
                templateUrl: "../Templates/people/all.html",
                controller: "allPeopleCtrl"
            }).when("/addPerson", {
                templateUrl: "../Templates/people/add.html",
                controller: "addPersonCtrl"
            }).when("/editPerson/:personId", {
                templateUrl: "../Templates/people/edit.html",
                controller: "editPersonCtrl"
            });
        }]);
})();
In the baseSvc.js, we have to use _spPageContextInfo.siteAbsoluteUrl instead of_spPageContextInfo.webAbsoluteUrl for baseUrl.
var baseUrl = _spPageContextInfo.siteAbsoluteUrl;
We are all done in developing SharePoint Hosted app using AngularJS. Another thing, I must mention that whenever you add any file into your project, please check the respective Elements.xml file. Scripts, Content and Images have their own Elements.xml file. For example, I have added template files inside Templates folder, So my main Elements.xml file becomes like the following:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="RootModule">
    <File Path="Templates\people\add.html" 
    Url="Templates/people/add.html" ReplaceContent="TRUE" />
    <File Path="Templates\people\all.html" 
    Url="Templates/people/all.html" ReplaceContent="TRUE" />
    <File Path="Templates\people\edit.html" 
    Url="Templates/people/edit.html" ReplaceContent="TRUE" />
  </Module>
</Elements>
Now right click on your project and click deploy. I hope you will be blessed with the following page.
app home

Conclusion

Hope you enjoyed this article. Now download my source code and start hacking it in your own way. Use comment section if you are stuck

4 comments:

  1. I wanted to leave a little comment to support you and wish you a good continuation. Wishing you the best of luck for all your blogging efforts. cnc router manufacturer

    ReplyDelete
  2. Leader in developing embedded system projects, providing Engineering and SCADA solutions using Raspberry pi, Arduino and more.... 3d printing

    ReplyDelete
  3. Superbly written article, if only all bloggers offered the same content as you, the internet would be a far better place. 91

    ReplyDelete