Angular and the User Information List

My latest effort was to give my customer an application that searches the User Information List, allows them to edit user properties, and displays the groups the user belongs to. This was built using AngularJS. My first tutorial on working with Angular is here: Angular.

The User Information List (UIL) is unique to each Site Collection, and contains every user who has access. In a typical setup, the source of user accounts is Active Directory, so each user in the UIL is either an AD user account or an AD group account. This allows SharePoint Administrators to use an existing AD Group, say for an entire Department, and give that group read-only access to the entire SharePoint Site Collection. When a user first logs onto a SharePoint site, he is added to the UIL. SharePoint groups are composed of users from the UIL. It is a best practice to grant permissions to a SharePoint group, and not to an individual user. That way, when you need to give someone else the same level of permissions, you simply add them to the same groups. 

Anyway, this article isn't about the UIL, there are plenty of articles online that go into greater detail. Tobias Zimmergren has a good article about interacting with the UIL here: https://zimmergren.net/sharepoints-hidden-user-list-user-information-listI just wanted to give you a little background as to why I needed to use Angular. The UIL is not like a typical SharePoint list. For one thing, you can't edit in datasheet view and update multiple users at once. Searching for a specific user in the UIL is not easy, and although you can see their information, most of the fields with the users are hidden. So, I decided to use my newly developed Angular skills to create a one page app to search for and edit users, and also to display their group membership. Believe it or not, there is no way out of the box to see what groups a user belongs to, even in SharePoint 2013.

One important note before I dive into the code: The SharePoint farm I am working with does not use the User Profile Service (UPS), which is a farm-level solution that lets you manage all of your user information in a central location. As I mentioned, each Site Collection's UIL is unique. If you have multiple Site Collections, changing a user's phone number in one Site Collection does NOT update that information in any other Site Collection. If you don't use scripting or some other mechanism to keep all your UILs in sync, a user could exist in one Site Collection but not in another. The User Profile Service is an excellent way to manage this problem, but it stores the information centrally. This application I have built would need to be changed if you are using UPS.

Files

As with the Cigars list example in Angular, I built a one page application. All the interaction occurs by reloading only a portion of the page, so the entire page is only loaded once. Here is a list of the files I used:

  • Display.html - Shows a single item, or user, in view mode. Also shows their groups.
  • Edit.html - Shows a single item, or user, in edit mode.
  • List.html - Shows all users in the UIL. Allows you to search for users.
  • Users.html - The home page, basically a placeholder for all the script references and the other three pages which actually have content.
  • Users.js - Contains all code.
There is another .js file, SPDataServices.js, which lives in the Scripts folder. I am treating it as another reference for this article, like angular.js. I detail that class here: SPDataServicesSPDataServices uses CSOM to talk to SharePoint, in this application I begin to use REST, but I already know I am using it the hard way. I have gotten pretty good at using the caveman method: Make it work, SOMEHOW, then figure out a more elegant way later. Hopefully I will come back and update this article when I learn more about how to consume REST properly.

Users.html

This is the home page for the application. If there is any html that needs to appear on every page, such as a menu or header or footer, it would be placed in this page. It serves two purposes: it contains a reference for every external script file, and it has a placeholder <div> tag where all the other pages with actual content are displayed.

​Users.html
​<script type="text/javascript" src="../SiteAssets/Scripts/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="../SiteAssets/Scripts/jquery-ui.min.js"></script>
<script type="text/javascript" src="../SiteAssets/Scripts/angular.min.js"></script>
<script type="text/javascript" src="../SiteAssets/Scripts/angular-route.js"></script>
<script type="text/javascript" src="../SiteAssets/Scripts/SPDataServices.js"></script>
<script type="text/javascript" src="../SiteAssets/PTS/Users/Users.js"></script>

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


Note the last reference is to Users.js, which contains the code specific to this application. You see the nested<div> tags, that is the placeholder for the content Angular will provide. Anything I would want to display on all pages, I would put it either above or below these <div> tags. The ng-app attribute "modUsers" tells Angular to give control of this container (the <div> tag) to the module named "modUsers". Thus, you could have several modules working on the same page, if you needed to. The ng-view attribute is the placeholder. The modUsers module will determine which html file and which controller is used, based on hash tags in the query parameters.  Tony Alicea has a great, great video on how to use the fragment identifier to render single page applications here: https://youtu.be/qvHecQOiu8g. I highly recommend watching it, and the rest of the series. He explains in very simple terms how to use hash tags (remember anchors? Does anyone use anchors in web pages anymore?) to essential mimic the functionality of Ajax, without all the overhead of Ajax. In simple terms, a single page application is loaded once, and a portion of the page is reloaded on demand. This means you don't see a flickering of the web page, which happens when you have to reload the entire page. This is a huge improvement over earlier web-based applications.

List.html

This page displays the first 100 users (I haven't build a paging mechanism yet). When a user enters a search criteria, the page is reloaded and filtered based on that criteria. Note the ng-keyup attribute in the txtSearch text box. That allows the user to click the Enter key and submit the search, without having to click on the Search button. Both actions trigger the vm.searchButtonClick() event. The first column in the table is link that uses hash tags to load Display.html in the placeholder.

​List.html
​<table cellpadding="5" border="0">
<tr>
<td style="font-weight: bold;">Search</td>
<td>
<input id="txtSearch" type="text" style="width: 400px;" ng-keyup="$event.keyCode == 13 &&vm.searchButtonClick()" ng-model="vm.Search" />

</td>
<td class="buttons" colspan="2">
<input type="button" value="Search" ng-click="vm.searchButtonClick()" />
</td>
</tr>
</table>

<table cellpadding="5" border="0">
<thead>
<tr>
<th ng-repeat="f in vm.fields" align="left" nowrap="nowrap">{{f}}</th>
</tr>
</thead>
<tr ng-repeat="u in vm.Users">
<td>{{u.ID}}</td>
<td nowrap="nowrap"><a href="#Display/{{u.ID}}">{{u.Title}}</a></td>
<td>{{u.Department}}</td>
<td>{{u.Office}}</td>
<td>{{u.JobTitle}}</td>
<td>{{u.MobilePhone}}</td>
<td>{{u.EMail}}</td>
<td>{{u.Name}}</td>
<td>{{u.Deleted}}</td>
<td>{{u.UserInfoHidden}}</td>
</tr>
<tr>
<td colspan="10" style="color: #CC0000; font-style:italic; font-weight: bold;">{{vm.Warning}}</td>
</tr>
</table>


Display.html

This page displays a single user in read only mode. It also displays the groups that the member belongs to.

​Display.html
​<table cellpadding="4">
<tr>
<td colspan="4">
<h2 style="font-weight: bold;">{{vm.Title}}</h2>
</td>
</tr>
<tr>
<td style="font-weight: bold;">Department</td>
<td>{{vm.Department}}</td>
<td style="font-weight: bold;">Office</td>
<td>{{vm.Office}}</td>
</tr>
<tr>
<td style="font-weight: bold;">Job Title</td>
<td>{{vm.JobTitle}}</td>
</tr>
<tr>
<td style="font-weight: bold;">Mobile Phone</td>
<td>{{vm.MobilePhone}}</td>
<td style="font-weight: bold;">Email</td>
<td>{{vm.EMail}}</td>
</tr>
<tr>
<td style="font-weight: bold;">Account</td>
<td colspan="3">{{vm.Account}}</td>
</tr>
<tr>
<td class="buttons" colspan="2">
<span ng-show="vm.UserCanEdit">
<input type="button" value="Edit" ng-click="vm.editButtonClick()" />&nbsp;&nbsp;
</span>
<input type="button" value="Close" ng-click="vm.closeButtonClick()" />
</td>
</tr>
<tr>
<td style="font-weight: bold;">Groups</td>
</tr>
        <tr ng-repeat="g in vm.Groups">
<td nowrap="nowrap" colspan="2"><a href="/_layouts/15/people.aspx?MembershipGroupId={{g.ID}}" target="_blank">{{g.Title}}</a></td>
</tr>
<tr>
<td class="errorMessage" colspan="2">{{vm.message}} </td>
</tr>
</table>


As you will see in Users.js, the controller (vm) is defined in the script file, not on the html page. The curly brackets {{vm.Title}} tells Angular to show the value of the Title property here. Note that we are not referencing the controller at all in the html file. There are two buttons, Edit and Close, which use the ng-click attribute to tie them to an event in the script file. Note also the use of the ng-show attribute, if "UserCanEdit" is false, the user will not see the Edit button. In the Edit.html file, this attribute will hide the entire page, in the unlikely event a user without permission gets to the Edit page.

At the bottom is a repeater (ng-repeat) that displays all the groups. Each row is a link to the standard Groups page, where you see the rest of the group members. I didn't recreate the out-of-the-box functionality for displaying group members and managing the group, since that still works fairly well.

In the code below, we have a simpler version of AngularJS (not a single page application), to show the differences.

Simple AngularJS example
​<div ng-app="SIG">
<table>
<tr>
<td style="vertical-align: top;">
<div ng-controller="Equipment as vm">
<h2>{{vm.title}}</h2>
<h3>{{vm.description}}</h3>
</div>
</td>
</tr>
</table>
</div>


Note the outer <div> tag with the ng-app attribute tells Angular to find the "SIG" module. Then, the inner <div> tag with the ng-controller attribute tells Angular to reference the "Equipment" controller within the "SIG" module, and adds the alias "vm" here in the html file. Angular will then populate the title and description properties within the table, as shown. This is a very simple version of AngularJS, and works perfectly fine when you have a simple data structure to display. But in a single page application, the controller and the alias are defined in the script file, and can be swapped out during execution. Thus, the list of items, the view of a single item, and the editing of single item can all be displayed without reloading the entire web page. Again, the power of Ajax, without all the overhead of Ajax.

Edit.html

This page displays a single user in edit mode. It is very similar to Display.html, the difference of course is using input controls for the editable fields. Note the Save and Cancel buttons.

​Edit.html
​<table ng-show="vm.UserCanEdit" cellpadding="4">
<tr>
<td colspan="4">
<h2 style="font-weight: bold;">{{vm.Title}}</h2>
</td>
</tr>
<tr>
<td style="font-weight: bold;">Department</td>
<td>
<input type="text" style="width: 200px;" ng-model="vm.Department" />
</td>
<td style="font-weight: bold;">Office</td>
<td>
<input type="text" style="width: 300px;" ng-model="vm.Office" />
</td>
</tr>
<tr>
<td style="font-weight: bold;">Job Title</td>
<td colspan="3">
<input type="text" style="width: 400px;" ng-model="vm.JobTitle" />
</td>
</tr>
<tr>
<td style="font-weight: bold;">Mobile Phone</td>
<td>
<input type="text" style="width: 200px;" ng-model="vm.MobilePhone" />
</td>
<td style="font-weight: bold;">Email</td>
<td>
<input type="text" style="width: 200px;" ng-model="vm.EMail" />
</td>
</tr>
<tr>
<td style="font-weight: bold;">Account</td>
<td colspan="3">{{vm.Account}}</td>
</tr>
<tr>
<td class="buttons" colspan="2">
<input type="button" value="Save" ng-click="vm.saveButtonClick()" />&nbsp;&nbsp;
<input type="button" value="Cancel" ng-click="vm.cancelButtonClick()" />
</td>
</tr>
<tr>
<td class="errorMessage" colspan="2">{{vm.message}}</td>
</tr>
</table>


Users.js

Finally, the script file. The module is named "modUsers", I brought along my Hungarian Notation habits from Visual Studio. I wanted to distinguish the module (all of the code that controls this application) from the "Users" controller (the code that controls a collection of users within the application). So, I am going to follow this naming convention in the future. Here is the entire file, I am going to break it up into sections to discuss it.

​Users.js
​(function () {

  var app = angular.module("modUsers", ["modSPDataServices", "ngRoute"]);

//
// Configuration
//

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

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

.when("/Search/:Search", {
templateUrl: "/SiteAssets/PTS/Users/List.html",
controller: "Users",
controllerAs: "vm"
})

.when("/Display/:ItemID", {
templateUrl: "/SiteAssets/PTS/Users/Display.html",
controller: "User",
controllerAs: "vm"
})
.when("/Edit/:ItemID", {
templateUrl: "/SiteAssets/PTS/Users/Edit.html",
controller: "User",
controllerAs: "vm"
})
.otherwise( {
template: "<h2>This is not a valid selection.</h2>"
})
  }]); // end app.config( ["$locationProvider", "$routeProvider", function($locationProvider, $routeProvider) {$locationProvider.hashPrefix(""); 
 
//
// User Controller
//

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

var vm = this;
var listItemID = $routeParams.ItemID;
vm.fields = ["ID", "Title", "Department", "Office", "JobTitle", 
"MobilePhone", "EMail", "Name"];
var savedFields = ["Department", "Office", "JobTitle", "MobilePhone", "EMail"];
vm.UserCanEdit = false;

$http.get(_spPageContextInfo.webAbsoluteUrl + "/_api/web/currentUser").then(function (Data) {
var xml = $.parseXML(Data.data);
if (xml.getElementsByTagName("d:Id")[0].textContent == listItemID) {
vm.UserCanEdit = true;
}
else {
$http.get(_spPageContextInfo.webAbsoluteUrl + "/_api/web/currentUser/issiteadmin").then(function (siteAdminData) {
var siteAdminXml = $.parseXML(siteAdminData.data);
if (siteAdminXml.getElementsByTagName("d:IsSiteAdmin")[0].textContent == "true") {
vm.UserCanEdit = true;
}
})
}
})
if (listItemID) {
SPDataServices.getListItem("User Information List", listItemID, vm.fields)
.then( function(result) {
vm.Title = result.get_item("Title")
vm.Department = result.get_item("Department")
vm.Office = result.get_item("Office")
vm.JobTitle = result.get_item("JobTitle")
vm.MobilePhone = result.get_item("MobilePhone")
vm.EMail = result.get_item("EMail")
vm.Account = result.get_item("Name")
vm.Groups = [];
$http.get(_spPageContextInfo.webAbsoluteUrl + "/_api/web/RoleAssignments/Groups").then(function (groupsData) {
var xml = $.parseXML(groupsData.data);
angular.forEach(xml.getElementsByTagName("entry"), function(entry) {
var group = {};
group["ID"] = entry.getElementsByTagName("d:Id")[0].textContent;
group["Title"] = entry.getElementsByTagName("d:Title")[0].textContent;
$http.get(_spPageContextInfo.webAbsoluteUrl +
  "/_api/web/sitegroups/getByName('" + group["Title"] + "')/Users?$filter=Id eq " + listItemID).then(function (usersData) {
  var usersXml = $.parseXML(usersData.data);
if (usersXml.getElementsByTagName("entry")[0] != undefined) {
vm.Groups.push(group);
}
})
})
})
})
.catch( function(failure) {
vm.message = failure;
console.log(failure);
});
}
vm.editButtonClick = function () {
window.location.href = "#Edit/" + listItemID;
}

vm.closeButtonClick = function () {
window.location.href = "#";
}
 
vm.saveButtonClick = function () {
SPDataServices.saveListItem("User Information List", listItemID, savedFields,
  [vm.Department, vm.Office, vm.JobTitle, vm.MobilePhone, vm.EMail])
.then(function(result) {
window.location.href = "#";
});
}

vm.cancelButtonClick = function () {
window.location.href = "#";
}  
}]); // end app.controller("User", ["$q", "$log", "$routeParams", "SPDataServices", function ($q, $log, $routeParams, SPDataServices)
//
// Users Controller
//
app.controller("Users", ["$q", "$log", "$routeParams", "SPDataServices", 
function ($q, $log, $routeParams, SPDataServices) {

var vm = this;
var queryString = "<View><Query><OrderBy><FieldRef Name='Title'/></OrderBy>";
var search = $routeParams.Search;
if (search) {
vm.Search = search;
queryString = queryString +
"<Where>\
<Or>\
<Contains>\
<FieldRef Name='Title'/><Value Type='Text'>" + search + "</Value>\
</Contains>\
<Or>\
<Contains>\
<FieldRef Name='Name'/><Value Type='Text'>" + search + "</Value>\
</Contains>\
<Contains>\
<FieldRef Name='JobTitle'/><Value Type='Text'>" + search + "</Value>\
</Contains>\
</Or>\
</Or>\
</Where>";
}
queryString = queryString + "</View></Query>";
vm.fields = ["ID", "Name", "Department", "Office", "Job Title", "Mobile Phone", "Email", "Account", "Deleted", "Hidden"];
var Controls = [vm.ID, vm.Title, vm.Department, vm.Office, vm.JobTitle, vm.MobilePhone, vm.EMail, 
vm.Name, vm.Deleted, vm.UserInfoHidden];
var txtSearch = document.getElementById("txtSearch")
if (txtSearch) {
txtSearch.focus();
}

  vm.Users = [];
SPDataServices.getListItems("User Information List", queryString, vm.fields)
.then(function(result) {
var max = 100;
if (result.length > max) {
vm.Warning = "There were more than " + max + " results returned.";
}
  for (var i = 0; i < Math.min(result.length, max); i++) {
  if (result[i]["Name"] != result[i]["Title"]) {
  vm.Users.push(result[i]);
  }
  }
})
.catch(function(failure) {
vm.failure = failure;
});
vm.searchButtonClick = function () {
window.location.href = "#Search/" + vm.Search;
}
}]); // end app.controller("Users", ['$q', "$log", function ($q, $log) {
}()); // end function


Configuration

The configuration section, or "block", contains code which is called once during the configuration phase. In this application, I am using the routeProvider service to determine which html page and controller goes in the placeholder, and is thus shown to the user. There is where the magic of Ajax without Ajax takes place. The "when" method does this by interrogating the query parameters. The when method is very similar to a switch statement in C#. If the URL matches the when statement, the templateURL, controller, and controllerAs (alias) are assigned. The otherwise method, like the default method in a switch statement, fires if there are no matches, and displays an error to the user. A user can manually enter anything in the URL, the otherwise is there to handle when this or some other error occurs.

Note that in a typical aspx page, we would use a querystring, with ? for the first element and & for all other elements. This would require reloading the entire page to change these elements. In a single page application, we use the hash tag and spoof additional folders. For example, the Web Part Page "Users.html" contains a Content Editor Web Part which references the application. The user would see the following:

  • /SitePages/Users.aspx - What the user sees at start up. There are no parameters, so the List.html page is shown.
  • /SitePages/Users.aspx#/Search/doug - The user entered "doug" as a search criteria. Note the #/. The user would likely assume that Search and doug are folders, but they are simply parameters. The List.html page is shown, and the Search parameter is now available to the controllers, to filter the results.
  • /SitePages/Users.aspx#/Display/7 - The user clicked the user with an ID of 7. The Display.html page is shown, and the ItemID parameter is now available to the controllers. Note that the Display parameter and the Display.html do not have to match, the when method determines what parameter points to what .html file. You could just as easily make the parameter View, as in "/SitePages/Users.aspx#/View/7", and point to the Display.html page. They are kept the same to avoid confusion.
  • /SitePages/Users.aspx#/Edit/7 - The user clicked the Edit button, and the Edit.html page is shown. The ItemID parameter is again made available to the controllers. Note that if a parameter is not defined in the when method, such as the Search parameter, it is not going to be available to the controllers. I am researching how to pass additional parameters, so that the users would see their search criteria again after updating a user.
Configuration
//
// Configuration
//

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

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

.when("/Search/:Search", {
templateUrl: "/SiteAssets/PTS/Users/List.html",
controller: "Users",
controllerAs: "vm"
})

.when("/Display/:ItemID", {
templateUrl: "/SiteAssets/PTS/Users/Display.html",
controller: "User",
controllerAs: "vm"
})
.when("/Edit/:ItemID", {
templateUrl: "/SiteAssets/PTS/Users/Edit.html",
controller: "User",
controllerAs: "vm"
})
.otherwise( {
template: "<h2>This is not a valid selection.</h2>"
})
  }]);


User Controller

This controller manages a single User. I use REST commands to talk to SharePoint, while the SPDataServices service uses CSOM. As I get more familiar with Angular, I plan to use REST everywhere. But like I said, I am using caveman methods. Find the first solution that does the job, then improve over time. If you are more familiar with REST than me, try not to laugh. I am converting it to xml and brute forcing the data I want. I already know that there is an easier way using JSON, that I will implement. But this method works.

The UIL is different from other lists, as I stated earlier. Users must be given read permissions to even see the list. But you must be a Site Collection Administrator to change anyone else's information. Here, the UserCanEdit property is set to false, then set to true if you are editing your own record. If not, it is set to true if you are a Site Collection Adminstrator. I am also using permissions for the entire Users folder in Site Assets to block users who don't have access.

The getListItem method returns the specific fields for the selected user, and assigns them to properties of the Controller (alias "vm"). I then use REST again to pull all the Site Groups, then loop through each one and filter for the current User ID. If it exists, I add that group to an array. The array is then displayed as a link on the Display.html page. One improvement you might notice is that I get the groups whether we are displaying or editing the user, it should only go through that step if we are in display mode.

Note that all the button click events change the window.location.href property to set the query parameters. This lets us go from one page to another, using the routeProvider service.

​User Controller
​        //
// User Controller
//

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

var vm = this;
var listItemID = $routeParams.ItemID;
vm.fields = ["ID", "Title", "Department", "Office", "JobTitle", 
"MobilePhone", "EMail", "Name"];
var savedFields = ["Department", "Office", "JobTitle", "MobilePhone", "EMail"];
vm.UserCanEdit = false;

$http.get(_spPageContextInfo.webAbsoluteUrl + "/_api/web/currentUser").then(function (Data) {
var xml = $.parseXML(Data.data);
if (xml.getElementsByTagName("d:Id")[0].textContent == listItemID) {
vm.UserCanEdit = true;
}
else {
$http.get(_spPageContextInfo.webAbsoluteUrl + "/_api/web/currentUser/issiteadmin").then(function (siteAdminData) {
var siteAdminXml = $.parseXML(siteAdminData.data);
if (siteAdminXml.getElementsByTagName("d:IsSiteAdmin")[0].textContent == "true") {
vm.UserCanEdit = true;
}
})
}
})
if (listItemID) {
SPDataServices.getListItem("User Information List", listItemID, vm.fields)
.then( function(result) {
vm.Title = result.get_item("Title")
vm.Department = result.get_item("Department")
vm.Office = result.get_item("Office")
vm.JobTitle = result.get_item("JobTitle")
vm.MobilePhone = result.get_item("MobilePhone")
vm.EMail = result.get_item("EMail")
vm.Account = result.get_item("Name")
vm.Groups = [];
$http.get(_spPageContextInfo.webAbsoluteUrl + "/_api/web/RoleAssignments/Groups").then(function (groupsData) {
var xml = $.parseXML(groupsData.data);
angular.forEach(xml.getElementsByTagName("entry"), function(entry) {
var group = {};
group["ID"] = entry.getElementsByTagName("d:Id")[0].textContent;
group["Title"] = entry.getElementsByTagName("d:Title")[0].textContent;
$http.get(_spPageContextInfo.webAbsoluteUrl +
  "/_api/web/sitegroups/getByName('" + group["Title"] + "')/Users?$filter=Id eq " + listItemID).then(function (usersData) {
   var usersXml = $.parseXML(usersData.data);
if (usersXml.getElementsByTagName("entry")[0] != undefined) {
vm.Groups.push(group);
}
})
})
})
})
.catch( function(failure) {
vm.message = failure;
console.log(failure);
});
}
vm.editButtonClick = function () {
window.location.href = "#Edit/" + listItemID;
}

vm.closeButtonClick = function () {
window.location.href = "#";
}
 
vm.saveButtonClick = function () {
SPDataServices.saveListItem("User Information List", listItemID, savedFields,
  [vm.Department, vm.Office, vm.JobTitle, vm.MobilePhone, vm.EMail])
.then(function(result) {
window.location.href = "#";
});
}

vm.cancelButtonClick = function () {
window.location.href = "#";
}  
}]);


Users Controller

This controller manages a collection of Users. It is very similar to the Cigars controller in my first tutorial, Angular. I just had to change the fields involved. At some point, I want to build an array that holds both the property, and the field name as a string, and that will simplify how I build these controllers. One step at a time. Note the search criteria is simply a CAML statement, that checks for the occurence of the search criteria anywhere in the Title (display name), Name (account), and Job Title fields. If you have First Name and Last Name fields exposed in your UIL, you could include them in the search, as well as Nickname, Email, Username, or whatever other fields you want to search against. In previous versions (in Visual Studio), I even included Phone Number, so you could look someone up by their phone number.

​Users Controller
//
// Users Controller
//
app.controller("Users", ["$q", "$log", "$routeParams", "SPDataServices", 
function ($q, $log, $routeParams, SPDataServices) {

var vm = this;
var queryString = "<View><Query><OrderBy><FieldRef Name='Title'/></OrderBy>";
var search = $routeParams.Search;
if (search) {
vm.Search = search;
queryString = queryString +
  "<Where>\
  <Or>\
  <Contains>\
  <FieldRef Name='Title'/><Value Type='Text'>" + search + "</Value>\
  </Contains>\
  <Or>\
  <Contains>\
  <FieldRef Name='Name'/><Value Type='Text'>" + search + "</Value>\
  </Contains>\
  <Contains>\
  <FieldRef Name='JobTitle'/><Value Type='Text'>" + search + "</Value>\
  </Contains>\
  </Or>\
  </Or>\
</Where>";
}
queryString = queryString + "</View></Query>";
vm.fields = ["ID", "Name", "Department", "Office", "Job Title", "Mobile Phone", "Email", "Account", "Deleted", "Hidden"];
var Controls = [vm.ID, vm.Title, vm.Department, vm.Office, vm.JobTitle, vm.MobilePhone, vm.EMail, 
vm.Name, vm.Deleted, vm.UserInfoHidden];
var txtSearch = document.getElementById("txtSearch")
if (txtSearch) {
txtSearch.focus();
}

  vm.Users = [];
SPDataServices.getListItems("User Information List", queryString, vm.fields)
.then(function(result) {
var max = 100;
if (result.length > max) {
vm.Warning = "There were more than " + max + " results returned.";
}
  for (var i = 0; i < Math.min(result.length, max); i++) {
   if (result[i]["Name"] != result[i]["Title"]) {
   vm.Users.push(result[i]);
   }
  }
})
.catch(function(failure) {
vm.failure = failure;
});
vm.searchButtonClick = function () {
window.location.href = "#Search/" + vm.Search;
}
}]); // end app.controller("Users", ['$q', "$log", function ($q, $log) {
}()); // end function


I have mentioned more than once that some of the code here is primitive, this is only my second finished AngularJS application (not just for practice). The principles I am using, such as routing, are sound, but the REST calls definitely need work. But I wanted to be documenting the work as it progresses, while my discoveries are fresh in my mind. There is a lot of information out there on Angular, even related to SharePoint, but concrete real world examples are hard to find.