Custom SharePoint Form Using Office UI Fabric and Angular

Author by Zavier Sanders

Building custom and fully interactive forms for SharePoint can often be complex. Using AngularJS with Office UI Fabric is a great way to build dynamic forms that have the same look and feel of Office and SharePoint.

 

In this article I am going to walk through the process of building a Employee Termination form using the SharePoint REST API, Office UI Fabric and Angular 1.x.

 

This article assumes you have a basic understanding of Angular. We’re not going to get too deep into the code, but will try to stay at a conceptual level. If you’re a developer and want to jump right into the code, click here to view the github repository.

 

Preview of the Solution

new-form-(1).PNG

 

What is Office UI Fabric?

The Office UI Fabric is a responsive, mobile-first, front-end framework for developers creating solutions for Office and SharePoint. The framework consists of styles and components that give your applications the same look and feel as Office and SharePoint. These components (textbox, peoplepicker, dropdown, date picker, table, etc) are basically a set of HTML and Javascript files you add to your application. Since we’re developing in Angular, we’ll be using a great open-source project called ngOfficeUIFabric. This project was started by Andrew Connell and is a community effort to create a collection of Office UI Fabric components as Angular directives.

Setting the Scene

I have created a custom list called Employee Termination which HR will submit when an employee leaves the company. Below is a screenshot of the columns created for the list.

 

list-columns-(2).PNG

 

Within the Site Assets library, I created a folder called Employee Termination. This is where all the files for the Angular application will be stored.

 

site-assets-(1).PNG

I also created a scripts folder to store files that are needed for our application. These files can be downloaded from a CDN using the links below.

 

 

site-assets-scripts.PNG

Building the Form

In this section we’ll begin creating the markup for the TerminationForm-Create.html new item form.

 

<!DOCTYPE html>

<html ng-app="myApp">

<head>

 <meta charset="utf-8">

 <meta http-equiv="X-UA-Compatible" content="IE=edge">

 <title>Employee Termination</title>

 <meta name="viewport" content="width=device-width, initial-scale=1">

 <style type="text/css">

   .section-divider

   {

     padding: 10px ;

     color: white;

     background-color: #004578 ;

     margin-bottom: 25px;

   }

 </style>

</head>

<body>

 <div ng-controller="MainController" class="ms-Grid">

   <h1>Employee Termination Form</h1>

   <br>

   <div>   

     <div>

       <!-- Message bar component -->

       <uif-message-bar>

         <uif-content>

           <h4>Instructions:</h4>

           <p>Employee must return all equipment.

           <br>

           Please fill out all applicable fields on the form.</p>

         </uif-content>

       </uif-message-bar>

     </div>

   </div>

   <br>

   <!-- HR Section -->

   <div class="ms-Grid-row ms-Grid">

     <h3 class="section-divider"><uif-icon uif-type="circleInfo"></uif-icon> Human Resource</h3>

     <div class="ms-Grid-col ms-u-sm6 ms-u-md6 ms-u-lg6">

       <!-- Textbox component -->

       <uif-textfield uif-label="Employee Name" ng-model="terminationRequest.Title"></uif-textfield>

       <!-- Dropdown component -->

       <uif-label>Department:</uif-label>

       <uif-dropdown ng-model="user.department">

           <uif-dropdown-option value="{{key}}" ng-repeat="(key, value) in Departments" title="{{value}}">{{value}}</uif-dropdown-option>

       </uif-dropdown>

       <!-- Date picker component-->

       <uif-label>Last Day of Work:</uif-label>

       <uif-datepicker ng-model="terminationRequest.LastDayOfWork"></uif-datepicker>

       <!-- Textbox component -->

       <uif-textfield uif-label="Reason for Termination" ng-model="terminationRequest.ReasonForTermination"></uif-textfield>

       <!-- Date picker component-->

       <uif-label>Next Job Start Date:</uif-label>

       <uif-datepicker ng-model="terminationRequest.NextJobStartDate"></uif-datepicker>

       <!-- Peoplepicker component -->

       <uif-label>Exit Interview Completed By:</uif-label>

       <uif-people-picker uif-people="getExitInterviewer" ng-model="terminationRequest.ExitInterviewer" uif-type="compact" uif-selected-person-click="personClicked" data-ng-click="getSelectedUserId(terminationRequest)" uif-search-delay="300" placeholder="Search for people">

         <uif-people-search-more>

           <uif-secondary-text>Showing {{asyncExitInterviewerResults.length}} results</uif-secondary-text>

           <uif-primary-text uif-search-for-text="You are searching for: ">Search organization people</uif-primary-text>

         </uif-people-search-more>

       </uif-people-picker>

     </div>

     <div class="ms-Grid-col ms-u-sm6 ms-u-md6 ms-u-lg6">

       <!-- Textbox component -->

       <uif-textfield uif-label="Remaining PTO Hours" ng-model="terminationRequest.PTO"></uif-textfield>

       <!-- Textbox component -->

       <uif-textfield uif-label="Next Place of Employeement" ng-model="terminationRequest.NewPlaceEmployment"></uif-textfield>

       <!-- Choice field component -->

       <uif-choicefield-group ng-model="terminationRequest.WasTwoWeekNoticeGiven">

           <uif-choicefield-group-title><label class="ms-Label is-required">Was Two Week Notice Given:</label></uif-choicefield-group-title>

           <uif-choicefield-option uif-type="radio" value="true">Yes</uif-choicefield-option>

           <uif-choicefield-option uif-type="radio" value="false">No</uif-choicefield-option>

       </uif-choicefield-group>

       <!-- Date picker component -->

       <uif-label>Date of Notice:</uif-label>

       <uif-datepicker ng-model="terminationRequest.DateOfNotice"></uif-datepicker

       <!-- Text area component -->

       <uif-textfield ng-model="terminationRequest.HRComments" uif-label="Comments:" uif-multiline="true"></uif-textfield>

       <div>

         <!-- Primary button components-->

         <uif-button type="button" data-ng-click="submitRequest(terminationRequest)" uif-type="primary">Submit</uif-button>

         <uif-button type="button" data-ng-click="cancel()" uif-type="command">Cancel</uif-button>

       </div>  

     </div>

   </div>  

 </div>    

 <!-- jQuery -->

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/jquery-3.1.0.min.js"></script>

 <!-- PickerJS -->

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/picker.js"></script>

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/picker.date.js"></script>

 <!-- Office UI Fabric stylesheets -->

 <link rel="stylesheet" href="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/fabric.min.css">

 <link rel="stylesheet" href="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/fabric.components.min.css">

 <!-- Angular & ngOfficeUiFabric -->

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/angular.min.js"></script>

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/scripts/ngOfficeUiFabric.min.js"></script>

 <!-- Application files -->

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/controllers.js"></script>

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/services.js"></script>

 <script src="/sites/Collaboration-Demo/SiteAssets/EmployeeTermination/app.js"></script>

</body>

</html>

 

In the markup above, the <html> tag is bound to myApp and the first <div> tag is bound to mainController. I’ve added comments above each Office UI Fabric component and bound them, using ng-model, to the terminationRequest scope variable defined in the controller.

 

You can see working demos and code examples of each Office UI Fabric component at http://ngofficeuifabric.com/demos/.  

 

App.js

var myApp = new angular.module('myApp', [

    'myApp.services',

    'myApp.controllers',

    'officeuifabric.core',

    'officeuifabric.components'

]);

 

The above code is the entry point of our application. First we create an Angular.js module. Then using dependency injection, we add references to our services and controllers modules we’ll create below. Also, don't forget to include the officeuifabric.core and officeuifabric.component modules.

Service.js

angular.module('myApp.services', [])

.factory('EmployeeTerminationFactory', function ($q, $http) {

    var webUrl = _spPageContextInfo.webAbsoluteUrl + "/_api/";

    var factory = {};

    //Create new termination request

    factory.createRequest = function(data) {

        var deferred = $q.defer();

        var createRequestURL = webUrl + "web/lists(guid'C723FCE2-D26C-42EC-B096-DBD6A43245DF')/Items";

        $http({

            url: createRequestURL,

            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);

            alert("Status Code: " + status);

        });

        return deferred.promise;

    };

    //Search for users

    factory.searchPeople = function(request) {

        var deferred = $q.defer();  

        $http({

            url: webUrl + "SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser",

            method: "POST",

            data: JSON.stringify({'queryParams':{

                    '__metadata':{

                        'type':'SP.UI.ApplicationPages.ClientPeoplePickerQueryParameters'

                    },

                    'AllowEmailAddresses':true,

                    'AllowMultipleEntities':false,

                    'AllUrlZones':false,

                    'MaximumEntitySuggestions':50,

                    'PrincipalSource':15,

                    'PrincipalType': 1,

                    'QueryString':request

                    //'Required':false,

                    //'SharePointGroupID':null,

                    //'UrlZone':null,

                    //'UrlZoneSpecified':false,

                    //'Web':null,

                    //'WebApplicationID':null

                }

            }),

            headers: {

                "accept": "application/json;odata=verbose",

                'content-type':'application/json;odata=verbose',

               'X-RequestDigest': document.getElementById('__REQUESTDIGEST').value

            }

        })

        .success(function (result) {

            var data = JSON.parse(result.d.ClientPeoplePickerSearchUser);

            var formattedPeople = [];

            var topResultsGroup = { name: "Top Results", order: 0 };

            if (data.length > 0) {

                angular.forEach(data, function (value, key) {

                    //Create user initials from user name

                    var name = value.DisplayText;

                    var userInitials = name.match(/\b\w/g) || [];

                    userInitials = ((userInitials.shift() || '') + (userInitials.pop() || '')).toUpperCase();

                    //Create people object, used for people picker component

                    formattedPeople.push({

                        initials: userInitials,

                         primaryText: name,

                         secondaryText: value.EntityData.Department,        

                         presence: 'available',

                         group: topResultsGroup,    

                         color: 'blue',

                         id: value.Key

                     });    

                });

                deferred.resolve(formattedPeople);

            }

        })

        .error(function (result, status) {

            deferred.reject(status);

            alert("Status Code: " + status);

        });

        return deferred.promise;

    };

     //Get user id

    factory.getUserId = function (UserName) {

        var deferred = $q.defer();

        $http({

            url: webUrl + "web/siteusers(@v)?@v='" + encodeURIComponent(UserName) + "'",

            method: "GET",

            headers: {

                "accept": "application/json;odata=verbose"

            }

        })

        .success(function (result) {

            deferred.resolve(result);

        })

        .error(function (result, status) {

            deferred.reject(status);

        });

        return deferred.promise;

    };

    return factory;
});

 

The code for the service has a few important features.

  • There is a dependency on $q. This service ensures that the functions run asynchronously and use their return values when the processing is completed. All functions use the $q.defer() function. This is used to declare the deferred object, that helps us in building an asynchronous function.

  • The code uses _spPageContextInfo.webAbsoluteUrl which represents the absolute URL for the sharepoint site where the app is located. * Make sure to change the list guids to match the guid for your SharePoint list.

  • The factory function createRequest() is used to create a new record in the Employee Termination list. It uses the terminationRequest $scope object passed from the controller.

  • The searchPeople() function is used to query user data for the people picker control. It uses the _api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser endpoint and does not require having to load any other dependent js files. This makes it very easy to implement multiple SharePoint people pickers using REST. Steve Curran’s SharePoint 2013 ClientPeoplePicker Using REST is an excellent article that goes into greater detail on implementing a client side people picker using the SharePoint REST API.

  • The data returned from the searchPeople() REST call is a string. Use JSON.parse to convert the response data into a JSON object array. Using Angular’s forEach method, we convert the returned user data into the correct format for the Office UI Fabric people picker directive.

  • The getUserId() function is used to retrieve the user Id for the user that get’s selected from the people picker control on the form. The user Id is required in order to save the user to the ExitInterviewer field, of the Employee Termination list. This function is called from the getSelectedUserId() scope variable in the controller.

Controller.js

angular.module('myApp.controllers', [])

.controller('MainController', function ($scope, $q, $timeout, EmployeeTerminationFactory) {

    init();

    function init() {

        //Define values for department dropdown directive

        $scope.Departments = {

            'Finance': 'Finance',

            'HR': 'HR',

            'IT': 'IT',

            'Sales': 'Sales'

        };

        //object used to store selected department

        $scope.user = {

            department: []

        };

        //Define the termination request scope object

        $scope.terminationRequest = {

           Title: '',

           EmployeeDepartment: '',

           LastDayOfWork: new Date().toISOString(),

           ExitInterviewerId: null,

           ExitInterviewer: [],

           NewPlaceEmployment: '',

           WasTwoWeekNoticeGiven: false,

           PTO: '',

           DateOfNotice: new Date().toISOString(),

           HRComments: '',

           NextJobStartDate: new Date().toISOString(),

           ReasonForTermination: '',

           __metadata: { 'type': 'SP.Data.EmployeeTerminationListItem' }

        };

    }

    var asyncExitInterviewer = [];

    var getPeople = function (people, searchQuery) {

        if (!searchQuery) {

          return [];

        }

        console.log("Searching for " + searchQuery);

        return people;

    }

    //Find userId when a user is selected from peoplepicker

    $scope.getSelectedUserId = function (data) {   

        //Set ExitInterviewerId

        var exitInterviewerId = data.ExitInterviewer[0].id;

        EmployeeTerminationFactory.getUserId(exitInterviewerId).then(function (results) {

            data.ExitInterviewerId = results.d.Id;

            console.log(results.d.Id);

            exitInterviewerId = null;

        });

    }

    //Create new termination request

    $scope.submitRequest = function (data) {   

        delete data.ExitInterviewer;

        data.EmployeeDepartment = $scope.user.department;

        console.log(data);

        EmployeeTerminationFactory.createRequest(data).then(function (results) {

            return results;

        });

        {history.go(-1);};

    }

    //Search users asynchronously

    $scope.getExitInterviewer = function (query) {

        var deferred = $q.defer();

        EmployeeTerminationFactory.searchPeople(query).then(function (results) {

            asyncExitInterviewer = JSON.parse(JSON.stringify(results));

            $scope.asyncExitInterviewerResults = getPeople(asyncExitInterviewer, query);

            if (!$scope.asyncExitInterviewerResults || $scope.asyncExitInterviewerResults.length === 0) {

                return $scope.asyncExitInterviewerResults;

            }

            deferred.resolve($scope.asyncExitInterviewerResults);

        });

        return deferred.promise;

    }

    //Remove user

   $scope.removePerson = function (person) {

     var indx = $scope.selectedAsyncPeople.indexOf(person);

     $scope.selectedAsyncPeople.splice(indx, 1);

   }

   //Cancel form and go back to previous page

   $scope.cancel = function() {

       history.go(-1);

   };

});

 

The code for the controller has the following features.

  • This controller uses the $scope, $q, and $timeout objects and the EmployeeTerminationFactory Angular factory.

  • Declares Departments array and user.department object. These will be used for data binding on the departments dropdown directive.

  • The getSelectedUserId() function calls the getUserId() function of the factory. This function gets called using ng-click on the ExitInterviewer people picker directive.

  • submitRequest() is used to submit a new termination request. Once the request is submitted, the user will be navigated back to the previous page.

  • The getExitInterviewer() function is used to return a promise resolving array of users for the people picker control.

Final Steps

In order to use the new custom form we just created, we need to modify the default new item form. Navigate to your Employee Termination list and open the Default New Form.

 

default-new-form.PNG

Edit the existing default web part and within the layout section, make sure to select “Hidden”. Then add a new Content Editor web part. Edit the new web part and add a relative URL to the TerminationForm-Create.html form we created earlier, then click OK to save your changes to the web part. Example - /sites/Collaboration-Demo/SiteAssets/EmployeeTermination/TerminationForm-Create.html

 

content-editor.PNG

 

Once the web part has finished saving, click “Stop Editing” to save your changes to the default new form. To test out the new form, just select New Item from the Employee Termination list and a custom form will be rendered.

Conclusion

In this article I’ve demonstrated how to create a responsive and custom form using the SharePoint REST API, Office UI Fabric, and Angular. The styles and components for the Office UI Fabric Angular directives are pretty straightforward and I highly recommend them to Office and SharePoint developers.

Tags in this Article