Creating a simple site directory for Office 365 with AngularJS

The purpose of this post, is to show you how easy it is to enhance your Office 365 solutions with a little bit of help from a JavaScript framework.

Usually I’d like to design my solutions around search since search is scalable, flexible, powerful and not that hard to setup. So in the case of a site directory, the solution usually comes down to:

  • Some sites (usually customer sites or project sites) are created at a certain path. Let’s say: /sites/customers/*
  • An indexer crawls these webs
  • I create a Result Source with a Query Transform: {searchTerms}* contentclass:STS_Web Path:http://site/sites/customers (or something along those lines)
  • A Content By Search Web Part (or Search Results) configured to use this Result Source
  • And of course a nice custom display template or hover panel to show all the UI goodness

Easy enough, right? However this might not be your best solution when using Office 365. You could use a similar approach, but with one big caveat. You have very little control over when and how a content source is crawled. I have seen reports, that index freshness is a serious issue to take into account. And people complaining it took days for search to return certain types of content. If that happens with your new sites, this would mean that it can take a while before people will see your new sites through the directory solution. And that is not a good thing of course.

Disclaimer and confession: this is not a drop in solution, so you have to adapt it to suite your needs: your mileage may vary. I wasn’t born a JS developer and actually learned most of what I know on this subject the past few years. I am not following best practices (JS/AngularJS), but this post should get you started.

 

Why AngularJS

I am not going explain a lot on why I prefer AngularJS over other frameworks, but in a nutshell:

  • It is a client side powerhouse. It does two-way data binding, has template support and utilizes dependency injection.
  • Promotes MVW style programming and behavior-view separation.
  • Testable (largely because of the MVW and dependency injection) and comes pre-bundled with mocks.
  • Backed by a large and active community so loads of support and extensions.
  • Since it does a lot, it can be your one-stop-shop for Single Page Applications.

If you want to read up on AngularJS, see the reading list at the end of the article. This includes also links to AngularJS Tutorials for those unfamiliar with the framework.

What to build

I am creating a Site Directory which is nothing more than a list with site titles. A simple search (as you type) input box allows for easy selection and navigation.

 

SiteDirectory

 

So the first objective is where to get the list of sites we are going to display. As stated in the intro, this could be search but we are going to use a more direct approach. AngularJS has awesome support for REST already build in and SharePoint exposes a lot of functionality through REST. This makes it the ideal method for connecting the two parts of the application.

REST in SharePoint is implemented in the following way:

You can access and interact with SharePoint resources using a constructed HTTP request. This request should target a SharePoint REST endpoint which can be constructed using the following steps:

  1. Start with the REST service reference: https://server/site/_api
  2. Specify the appropiate entry point, in my case I need to find subwebs: https://server/site/_api/web
  3. From the entry point, navigate to the resource you need to access. Again in my case subwebs (SPWebs): https://server/site/_api/web/webs
  4. Filter or select fields as appropriate: https://server/site/_api/web/webs?$select=Title, ServerRelativeUrl, id

This is the resulting JSON (depending on the requested odata verbosity):

json

So, we have all the web metadata we need to wire up our AngularJS app.

The steps

  1. First we need a basic structure for our small “app”. It will consist of some JS files and a HTML snippet we can use with a Content Editor Web Part. Surely not the preferred method, but will do fine for this simple site directory sample.
Controllers/Controllers.js Contains our single controller
Services/Services.js Contains our service for connecting to the SharePoint REST endpoint
Angular.min.js AngularJS framework
Angular-cookies.min.js AngularJS cookiefactory
App.js Our main “application”
myCustomerApp.txt HTML snippet for use with the Content Editor Web Part

 

  1. Let’s start with creating our service to pull the data in:
angular.module('myApp.services', [])

.service('$SharePointService', ['$q', '$http', '$cacheFactory', function ($q, $http, $cacheFactory) {
    
	this.getAllSites = function ($scope) {
		return $http({
            method: 'GET',
            url: '/sites/customers/_api/web/webs?$select=Title, ServerRelativeUrl, id',
            cache: false,
            headers: { "Accept": "application/json; odata=verbose" }
        });
	}
}]);

One method, getAllSites that uses the built-in $http directive to get the data from the REST endpoint. I also inject the $cacheFactory so I can switch the cache on if needed.

  1. Next, we setup our only controller to utilize this service and
angular.module('myApp.controllers', [])

.controller('sitesController', ['$scope', '$SharePointService','$cookieStore', function ($scope, $SharePointService, $cookieStore)  {
	$scope.sites = []
	
	$SharePointService.getAllSites($scope)
		.then(
            function (data, status, headers, config) {
				angular.forEach(data.data.d.results, function (site) {
					$scope.sites.push({
						title: site.Title,
						url: site.ServerRelativeUrl,
						id: site.Id,
						fav: $cookieStore.get('myFavoriteSites')===site.Id
					});
				});
            },
            function (data, status, headers, config) {
                console.log("Error " + status);
            }
		);
		
	$scope.addFavourite = function (site) {
		$cookieStore.put('myFavoriteSites',site.id);
		site.fav = true;
	};
	
	$scope.clearFavourites = function () {
		$cookieStore.remove('myFavoriteSites');
	};

}]);

Besides the $SharePointService, the controller also gets the $cookieStore injected. I use this to store the site.id and have people store their favorite site. In this example you can only store 1 favorite, but you could of course extend on this.

  1. Finally our app.js which bootstraps AngularJS and wires up the controller, service and cookie factory.
var myApp = angular.module('myCustomerApp', [
    'myApp.services',
    'myApp.controllers',
	'ngCookies'
]);
  1. To display our app to the user, I will use a Content Editor Web Part with a link to a html snippet. The snippets contains the references to the different JavaScript files, a HTML structure and some basic CSS. I omitted the CSS to keep it readable:
<div ng-app="myCustomerApp">
	<div ng-controller="sitesController" id="customersites">
		<input ng-model="search.title"><br>
			<ul>
				<li class="blue" ng-repeat="site in sites | filter:search | orderBy:'fav':true">
					<div class="left"><a href="{{site.url}}" ng-bind="site.title"></a></div>
					<div ng-class="{favourite:site.fav}"><a href="#" ng-click="addFavourite(site)"><i class="fa fa-star"></i></a></div>
				</li>
			</ul>
		<a href="#" ng-click="clearFavourites()">Clear favourites</a>
	</div>

	<!-- AngularJS -->
	<script src="/sites/customers/SiteAssets/myCustomerApp/angular.min.js"></script>
	<script src="/sites/customers/SiteAssets/myCustomerApp/angular-cookies.min.js"></script>
	<script src="/sites/customers/SiteAssets/myCustomerApp/Controllers/Controllers.js"></script>
	<script src="/sites/customers/SiteAssets/myCustomerApp/Services/Services.js"></script>

	<!-- Main App -->
	<script src="/sites/customers/SiteAssets/myCustomerApp/app.js"></script>
</div>

Couple of things to note:

  • A very simple HTML structure is used, in this case an unordered list.
  • This UL gets build by Angular’s ngRepeat directive.
  • The list of sites is sorted (ordered by) on the “fav” attribute to make sure the favorite site is list first.
  • A standard HTML <input> tag is added and Angular’s filter function is used to process the input for the ngRepeat directive. Because of the data-binding feature of Angular, this enables the “search as you type” experience.

All that we need to do now is deploy the files to a library on our SharePoint or Office 365 site. In my case I used the Site Assets library as part of the publishing feature. Once the files are uploaded, we need to adjust the path in our HTML snippet. Make sure you also check the url used by the service. This should point to your root directory site. Add a Content Editor Web Part to any page and set the content link to the HTML snippet file:

CEWP

Once saved and published, the page should show the Site Directory running.

The download link provided below includes the sample code mentioned in this article. The code contains a link to an external resource: a reference to FontAwesome.css on a CDN (//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css). Make sure this is what you want or remove the line and update the HTML snippet. Use at your own risk.

Download the sample code here: Blog.Examples.SiteDirectory.zip

 

My Curation on AngularJS http://curah.microsoft.com/83315/angularjs-notes-to-self
Getting started with REST in SharePoint 2013 http://msdn.microsoft.com/en-us/library/office/fp142380(v=office.15).aspx
Determine SharePoint REST service endpoint http://msdn.microsoft.com/en-us/library/office/dn292556(v=office.15).aspx

 

/Y.

Advertisements

4 comments

  • Hi Yuri, I am looking for a way to show a list of sites for which a user is authorized. I tried the content search web part but this only worked with the cross site pub feature enabled. The O365 subscription I’m working with does not support this feature.
    So instead I tried your Site Directory script. I configured it for one of my sites and it works fine when I’m logged on as a normal O365 user. When I share the site with an external user however the script shows nothing.
    I guess the external user must be authorized first to be able to use te REST api. Unfortunately I’m not a developer and do not have a clue how to get the script working for this scenario. I don’t want anonymous access to be able to access my sites. Can you help me?

    Regards Paul

    • Hi Paul,

      In this case, I guess you could change the REST query to leverage the REST Search Api and use a specific Result Source for this. External users should be able to perform queries on that. You would have to modfiy the code a bit, since the resulting JSON would be different. Hope this helps!

      Cheers, Yuri

  • Hi, I installed this on my o365 tenant but it only displays my list of sites when the page that contains my content editor web part is in ‘Edit’ mode. As soon as I save my page the list disappears and all I see is the search field and one blank row. Do you know of a workaround? I am using a team site.

    • Hi,

      Thanks for trying it out! Not exactly sure why you are experiencing this issue, but it could be one of the following:

      • It could be that the Javascript is removed from the CEWP upon saving. Instead of adding the JS directly in the CEWP, save it in a file in one of your libraries (Site Assets, Style Library, ….). Then link to this file using the Content Editor Web Part properties (Content Link) and see if that helps.
      • It could also be that MDS (Minimal Download Strategy) is messing up with the loading of the script. You might want to disable that feature and see if that is the cause.

      /Y.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s