Friday, January 23, 2015

Single Page Apps in SharePoint – Lessons Learnt…

 

Recently I developed a single page SharePoint Hosted App with Angular JS and SharePoint Client Side Object Model. In this post, I am sharing the lessons learnt and some design suggestions to those who are interested in taking this route.

Feel free to leave your comments, if you have any questions about these ideas.

Get rid of the complex Url…

First, an Angular SPA’s routing mechanism requires the views to be loaded with {Your Main Page}#{view name} syntax. But when SharePoint launches the App, it provides several parameters which are required to call back to SharePoint services. Honestly, I hate those parameters so first time my app is launched, I store those into cookies and redirect to my SPA home page without those parameters. This makes the App URLs much more SPA friendly. It’s hard to put the code here because there are many moving pieces, but if you are interested in this approach, I recommend following course by Andrew:

Building SharePoint Apps as Single Page Apps with AngularJS

In addition to what Andrew’s approach, I also provided an deep linking ability in my app so that I can include links to item pages in emails being sent out from the App.

Store Configuration in Hidden List…

I prefer to store some absolutely critical but configurable items in a hidden configuration list which the support people (including me in some cases) can modify to slightly tweak the application behavior. Example of such configuration is given below:

SNAGHTML5a6c971

As you can see, I store the configuration about my Managed Metadata Controls, shared mail box, subject and body of my mailto links, whether or not the names are lync enabled and so on… I wrote the app to take these values instead of hard-coding them into the app. After all, I feel bad if the business has to come to me for every little change that they want to make so I always try to find a balance between amount of time it takes to make it configurable vs. the value it provides.

Store Data outside app-web (personal choice)…

You can store the data inside the app-web or outside of the app web, for instance, into the Host Web. I know this is a big topic and there are lot of conflicting opinions. Honestly, it drives me crazy that the data inside the app web is not searchable, that it gets deleted every time you re-install the app and that you lose rich functionality like Alerts. I don’t know about you, but these limitations are not acceptable to me. Why should I lose my data every time I redeploy the app into my development site? Am I supposed to enter the test data again (of course, you can write some script to insert the test data)?

Since I stored the data outside the app, I included links to this lists from my an administration page in my App (see below). Navigating through different things is not easy in SharePoint, I have heard a lot of complaints about it, so providing a one click access to all important things goes a long way, my users loved it.

SNAGHTML5b644b2

The question is how do these lists get created and configured? A truly independent app can provide a configuration page from which you can trigger JavaScript (REST/JSOM) scripts and have these lists created/configured. If you want to be more sophisticated, you can check for a flag on host web (Hint: Web Property) indicating a missing configuration and  then complete run the configuration script and turn on the flag so that it does not run again.

Considering tight deadlines though, I used custom PowerShell Scripts which uses a CSOM object model to create this lists and libraries. You may wonder why CSOM object model? It’s because I wanted it run them without accessing the servers’ powershell console, imagine O365!

Get hold of People Picker and Managed Metadata Controls…

There is a shortage of SPA friendly People Picker and the Managed Metadata pickers. I chose to get Javascript based implementation of these controls from /OfficeDev/Pnp (link below)

https://github.com/OfficeDev/PnP/tree/master/Components

And then, I wrote Angular JS Directives on top of these controls. If you want to know more about this, leave a comment, I can share some code pieces.

Centralize SharePoint Data Access…

While this is a no-brainer, I still see developers writing random JavaScript on scattered across the pages. Make a point to centralize all data access in logically separated repositories. Angular provides a great way to build Services, make a use of them and put your logic in repository services created in Angular.

Cache.. Cache.. Cache..

The data that is not frequently updated should be cached. I liked Angular Local Storage module (link below) which allowed me to store things like quick links, contacts, currently logged in user’s info and many other things into a local session store. It also allows you to use both HTML5 local storage and traditional cookie storage, use them as per your need.

https://github.com/grevory/angular-local-storage

This greatly improved the responsiveness of my pages. I also put refresh icon where such data was displayed on screen which allowed users to refresh the data from server on-demand.

Allow Business to change Email Templates

Emails are so common into a custom SharePoint Apps and yet, many developers hard code the email templates into the code. I believe email templates should be allowed to be changed by power users (let your power users feel powerful), so I built custom admin pages which allows my users to change the body of my email templates. On the email template edit screens, I provided them a list of special placeholders (example $$CURRENT_USER_ID$$, $$ITEM_TITLE$$, etc) which my application replaces with real values before sending out emails. That way, they had complete control over the subject and body of emails, the well-documented placeholders allowed them to make those template truly dynamic and useful.

How to display gentle reminders for incomplete SharePoint User Profile?

Ability to find people in organization and skills is one of my favorite feature of SharePoint. However, it's obvious that it's useful only when employees complete and keep their profiles up to date. At minimum, I see a great value in completing user's office location, department, contact information, manager information, profile picture, "About Me" and skills information. While some of these pieces of information (like department, manager, office location etc.) comes from Active Directory (or other any source for that matter), information such as "About Me", Profile Picture, "Ask me about" are best filled (and should beJ) out by users. But many employees don't take a few moments to provide this information and there is no way (as far as I know) to send them automatic reminders about their incomplete profile. Well, who likes to be overwhelmed with those reminders?

So I thought of giving them a gentle reminder when they visit a certain page, like an intranet home page. The reminder could be in the form of short-living (5 seconds), one liner message that appears on top right corner when one or more profile property is not filled out. Example below:

I created a Web Part which can be dropped on the page where you want the users to see these gentle reminders. The web part can be configured with:

  1. Tokenized Error Message. Allowed tokens are {0} which will be replaced with link to user profile and {1} which will be replaced with comma separated list of properties which are NOT filled out
  2. Comma separated list of profile properties that you want to check for completeness

Well, it can be enhanced with other things, but enough to get started right now:

  1. Start Date and End Date (imagine you are running a "profile complete" campaign)
  2. Time Frame (display messages only during mornings?)

Most of the code resides in JavaScript, a snippet of which is given below:

   1:  <SharePoint:ScriptLink ID="ScriptLink3" name="SP.Runtime.js" runat="server"
   2:   
   3:     ondemand="false" localizable="false" loadafterui="true" />  
   4:   
   5:  <SharePoint:ScriptLink ID="ScriptLink1" name="SP.js" runat="server"
   6:   
   7:      ondemand="false" localizable="false" loadafterui="true" />  
   8:   
   9:  <SharePoint:ScriptLink ID="ScriptLink2" name="SP.UserProfiles.js" runat="server"
  10:   
  11:      ondemand="false" localizable="false" loadafterui="true" />  
  12:   
  13:  <script type="text/javascript">  
  14:   
  15:  "use strict";  
  16:   
  17:      $(function () {  
  18:   
  19:  var personProperties;  
  20:   
  21:  var propertiesToCheck = "COMMA SEPARATED LIST OF PROPS";  
  22:   
  23:  var notifyMessage = "TOKENIZED ERROR MESSAGE GOES HERE";//
  24:   
  25:  // Ensure that the SP.UserProfiles.js file is loaded before the custom code runs.
  26:   
  27:          SP.SOD.executeOrDelayUntilScriptLoaded(getUserProperties, 'core.js');  
  28:   
  29:   
  30:  function getUserProperties() {  
  31:   
  32:   
  33:  // Get the current client context and PeopleManager instance.
  34:   
  35:  var clientContext = new SP.ClientContext.get_current();  
  36:   
  37:  var peopleManager = new SP.UserProfiles.PeopleManager(clientContext);  
  38:   
  39:   
  40:  // Get user properties for the target user.
  41:   
  42:  // To get the PersonProperties object for the current user, use the
  43:   
  44:  // getMyProperties method.
  45:   
  46:   
  47:              personProperties = peopleManager.getMyProperties();  
  48:   
  49:   
  50:  // Load the PersonProperties object and send the request.
  51:   
  52:              clientContext.load(personProperties);  
  53:   
  54:              clientContext.executeQueryAsync(onRequestSuccess, onRequestFail);  
  55:   
  56:          }  
  57:   
  58:  // This function runs if the executeQueryAsync call succeeds.
  59:   
  60:  function onRequestSuccess() {  
  61:   
  62:  var emptyProps = new Array();  
  63:   
  64:              $.each(propertiesToCheck, function (index, prop) {  
  65:   
  66:  if (SP.ScriptUtility.isNullOrEmptyString(personProperties.get_userProfileProperties()[prop.name])) {  
  67:   
  68:                      emptyProps.push(prop.title);  
  69:   
  70:                  }  
  71:   
  72:              });  
  73:   
  74:  if (emptyProps.length > 0) {  
  75:   
  76:                  SP.UI.Notify.addNotification(notifyMessage.replace("{0}", personProperties.get_userUrl()).replace("{1}", emptyProps.join(", ")), false)  
  77:   
  78:              }  
  79:   
  80:   
  81:   
  82:          }  
  83:   
  84:   
  85:  // This function runs if the executeQueryAsync call fails.
  86:   
  87:  function onRequestFail(sender, args) {  
  88:   
  89:   
  90:              SP.UI.Notify.addNotification("Error in Profile Notify Widget: " + args.get_message());  
  91:   
  92:   
  93:          }  
  94:   
  95:      });  
  96:   
  97:   
  98:  </script>  





  1.  


I think this is a simple but very powerful technique which may help our clients get the most out of SharePoint User Profiles. Hope this helps!