Tuesday 28 April 2015

Using the Client-side Taxonomy Picker Control in an AngularJS directive on Office365

In my last post I wrote about using the Microsoft client-side people picker within an AngularJS directive.

In this post I want to take a look at using the client-side taxonomy picker wrapped up as an AngularJS directive, in an app running on an Officer 365 site. In a subsequent post I'll expand on the how the code works, but this post is just intended to give a quick overview of it.

You can download the code from Github here: https://github.com/matthewyarlett/AngularJS-Client-side-Taxonomy-Picker-Directive

The Taxonomy Picker control comes from the Office 365 Developer Patterns and Practises site.

The files for this app are uploaded to a folder on the root of the site. The apps parent file, app.html, is added to a page via Content Editor Webpart.

The app looks like this:


To use the directive, follow these basic steps.

1. Add the CSS and script references to your apps html page.

The Taxonomy Picker control comes with it's own CSS, so you need to include this.

<link href="../angularjs-taxonomy/Styles/taxonomypickercontrol.css" rel="stylesheet" />

You need to add a reference to the JS file the directive is declared in, as well as all of the Microsoft Scripts that the SharePoint Taxonomy Picker requires, as well as the scripts the app requires.

<!-- Load the constant variables -->
<script type="text/ecmascript" src="../angularjs-taxonomy/config/config.constants.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/models/models.js"></script>
<!-- Load third party scripts -->
<!-- AngularJS, Sanitize, resource -->
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/angular.min.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/angular-sanitize.min.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/angular-resource.min.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/angular-route.min.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/ui-bootstrap.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/jquery-1.9.1.min.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/scripts/taxonomypickercontrol.js"></script>
<!-- All of this scripts are used to create the app. -->
<script type="text/ecmascript" src="../angularjs-taxonomy/app.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/config/config.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/config/config.taxonomy.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/common/common.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/common/logging.js"></script>
<script type="text/ecmascript" src="../angularjs-taxonomy/controllers/controllers.js"></script>

2. Add the directive as a dependency to your app.

(function () {
    'use strict';        
    var app = angular.module('app', [
      //inject other Angular Modules 
      'ngSanitize',
      'ngResource', 
      'ui.bootstrap',
      //add the tax picker directive
      'ui.TaxonomyPicker',
      //inject App modules
      'common'
    ]);
})();

3. Add the directive to the page (you can add multiple instances of it).

When add the directive, it has several attributes that can be set to control the behaviour of the taxonomy picker.

Most aren't mandatory, but you must add the data model (via ng-Model), and you must add the pp-ready-to-load attribute (which tells the directive the data model has updated, and is ready to be used).

Possible attributes and they're values
AttributePossible valuesDefault valueRequired?
data-ng-modelAn array, containing a list of terms, in the following format:

var termsArray = [{ 'Name': object.TermLabel, 'Id': object.TermGuid}]
nullYes (you must supply a model to the data-n-model attribute, but it contain a null value)
data-pp-ready-to-loadtrue | falsefalseYes (the taxonomy picker will not render until this value is set to true)
data-pp-is-multi-valuedtrue | falsefalseNo
data-pp-widthAny numerical value, followed by 'px'220pxNo
data-pp-termsetidThe GUID of the termset.
E.g.
a5bd4ff2-2823-4ea2-8dd6-2d02c845493b
nullNo
data-pp-is-hashtagstrue | falsefalseNo
data-pp-is-keywordstrue | falsefalseNo

An example of adding a the taxonomy picker that references a termset;


<div ui-Taxonomy ng-model="vm.terms" pp-ready-to-load="{{vm.loadTaxonomyPickers}}" pp-termsetid="a5bd4ff2-2823-4ea2-8dd6-2d02c845493b" pp-is-multi-valued="{{true}}" id="taxPickerGeography" ></div>

An example of adding a the taxonomy picker that references the hashtags termset;


<div ui-Taxonomy ng-model="vm.hashtags" pp-ready-to-load="{{vm.loadTaxonomyPickers}}" pp-is-hashtags="{{true}}" pp-is-multi-valued="{{false}}" id="taxPickerHashtags" ></div>

An example of adding a the taxonomy picker that references the keywords termset;


<div ui-Taxonomy ng-model="vm.keywords" pp-ready-to-load="{{vm.loadTaxonomyPickers}}" pp-is-keywords="{{true}}" pp-is-multi-valued="{{true}}" id="taxPickerKeywords" ></div>

4. Load the data for the model.

Before initialising the directive (via changing the value of the property used in the data-pp-ready-to-load attribute to true), ensure you up the data model used for the taxonomy picker with the current data (if any).

For example, you might perform a REST call to get the current values in a list item. You would then create the model for the taxonomy picker when data from the REST call is returned.

After updating the data in the taxonomy pickers model, you would then change the value of the property used in the data-pp-ready-to-load attribute to true.

The code below shows an example of doing this.


//App Controller
(function () {
   'use strict';
   var controllerId = 'appCtrlr';
   angular.module('app').controller(controllerId, ['$scope', appCtrlr]);

   function appCtrlr($scope) {
      var vm = this;   
      vm.data = [];    
      vm.context = null;
      vm.terms = [];
      //The data model for the taxonomy picker requires an array of values. Each value 
      //represents a term, and has the Id and Name properties. The Id property takes the 
      //terms TermGuid and the Name property correlates to the terms Label property.       
      vm.terms = [{Id:"dc146209-c1b2-697e-c0e2-9259ba19e750",Name:"Failed Inspection"}];
      vm.keywords = [];
      vm.hashtags = [];
      vm.loadTaxonomyPickers = true;
      init();
      function init() {   
         //init code.
         //e.g. get data via REST and set the data model.                   
      }
   }
})();


As a side note, when you perform a REST call to Office365 or SharePoint, for a taxonomy
field, the data comes back as an object that has the TermGuid and Label properties, as seen in the following screen capture from Fiddler:



Internally, the Microsoft Client-side Taxonomy control requires the field properties as Id and Name,
which is why we format the input object with those properties above.

However, for the sake of usability, you can also pass in the raw value from a REST call. In this case, the init function might look like this (no formatting of the data takes place):

function init() {   
   getData().then(function(data){
      vm.data = data;
      vm.loadTaxonomyPickers = true;
      if (!$scope.$root.$$phase) {
         $scope.$apply();
      }
   }) 
}

And the HTML for the control, might look like this (note the vm.data.tags.results object that is passed in as the model).

<div ui-Taxonomy ng-model="vm.data.Tags.results" pp-ready-to-load="{{vm.loadTaxonomyPickers}}" pp-termsetid="a5bd4ff2-2823-4ea2-8dd6-2d02c845493b" pp-is-multi-valued="{{true}}" id="taxPickerTags" ></div>

That's it - as far as initialising and using the control goes.

To use the value(s) of a the taxonomy picker directive while the page is open, simple reference the taxonomy picker model, like you would any other property within a controllers data model.

<span data-ng-repeat="r in vm.terms track by $index">
    {{r.Name}} <span style="color:BURLYWOOD;">(Path: {{r.PathOfTerm}})</span><span data-ng-if="!$last">,&nbsp;</span>
</span>

Persisting the value(s) of the a taxonomy picker control is just as easy. You use the data in the taxonomy pickers model to write back to the server.

In the example below, I have a model used in a REST call for updating a list item that has a taxonomy field. Data from the model (used by taxonomy picker directive) is formatted for the REST call. Each term in the array of terms is transformed into a term string, that can be passed to the server via the REST call.

sr.models.shipInfoModel = function (id) {
   this.Id = id ? id : -1;
   //"pa28754972f84ff7a6bf3e4762f23e23" is the hidden text field (automatically) created for the taxonomy field
   //This is the field that you need to use to update the taxonomy field via REST.
   //You need to format selected terms as GUID|Label pairs
   this.pa28754972f84ff7a6bf3e4762f23e23;
   this.__metadata = {
      type: 'SP.Data.SrShipRegisterListItem'
   };
}

function populateShipInfoModel(srcModel, tagsTerms) {
   var dstModel = new sr.models.shipInfoModel(srcModel.Id);   
   //Update the taxonomy field values 
   if(srcModel.Tags && srcModel.Tags.results){
      if(srcModel.Tags.results.length == 0){
         dstModel.pa28754972f84ff7a6bf3e4762f23e23 = '';
      }else{
         for(var i = 0; i < srcModel.Tags.results.length; i++)
         {
            var t = srcModel.Tags.results[i];
            dstModel.pa28754972f84ff7a6bf3e4762f23e23 = dstModel.pa28754972f84ff7a6bf3e4762f23e23 + ';' + t.Name + '|' + t.Id;   
         } 
      } 
   }
   dstModel.__metadata.etag = srcModel.__metadata.etag;
   dstModel.__metadata.id = srcModel.__metadata.id;
   dstModel.__metadata.uri = srcModel.__metadata.uri;
   return dstModel;
}

And that's it!

Tuesday 21 April 2015

Using the Microsoft Client-side People Picker as an AngularJS Directive

[UPDATED]: I've written an updated post for this that include some feature enhancements and fixes. You can read that post here: (Updated) Using the Microsoft Client-side People Picker as an AngularJS Directive

Last year I wrote about using the Microsoft Client Side People Picker with an AngularJS app (here).

In this post, I'm building on that idea, and I'm going to demonstrate how to use the People Picker within an AngularJS Directive. Using a directive simplifies the code and makes it much more re-usable!

I'm going to break the post up into parts. This part will outline how to use the directive. The next post will cover a bit more about how the directive was created and how it works.

The code example is up on GitHub, here: AngularJS-Directive-for-SharePoint-People-Picker

Let's get into it!

1. Add the script references to your apps html page.

You need to add a reference to the JS file the directive is declared in, as well as all of the Microsoft Scripts that the SharePoint People Picker requires

<!-- Load the constant variables -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/config/config.constants.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/models/models.js"></script>
<!-- Load third party scripts required by the people picker -->
<script type="text/ecmascript" src="/_layouts/15/SP.UI.Controls.js"></script>
<script type="text/ecmascript" src="/_layouts/15/clienttemplates.js"></script>
<script type="text/ecmascript" src="/_layouts/15/clientforms.js"></script>
<script type="text/ecmascript" src="/_layouts/15/clientpeoplepicker.js"></script>
<script type="text/ecmascript" src="/_layouts/15/autofill.js"></script>
<script type="text/ecmascript" src="/_layouts/15/sp.RequestExecutor.js"></script>
<!-- AngularJS, Sanitize, resource -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular-sanitize.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular-resource.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/angular-route.min.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/ui-bootstrap.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/scripts/xml2json.min.js"></script>
<!-- All of this scripts are used to create the app. -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/app.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/config/config.js"></script>
<!-- ****** The following script contains the people picker directive. ****** -->
<script type="text/ecmascript" src="../angularjs-peoplepicker/config/config.peoplepicker.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/common/common.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/common/logging.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/services/dataservices.js"></script>
<script type="text/ecmascript" src="../angularjs-peoplepicker/controllers/controllers.js"></script>

2. Add the directive as a dependency to your app.

(function () {
    'use strict';        
    var app = angular.module('app', [
    //inject other Angular Modules
    'ngSanitize',
    'ngResource', 
    'ui.bootstrap',
    //inject the People Picker directive
    'ui.People',
    //inject App modules
    'common'
    ]);
})();

3. Add the directive to the page (you can add multiple instances of it).

When add the directive, it has several attributes that can be set to control the behaviour of the people picker.

Most aren't mandatory, but you must add the data model (via ng-Model), and you must add the pp-ready-to-load attribute (which tells the directive the data model has updated, and is ready to be used).

Possible attributes and they're values
AttributePossible valuesDefault valueRequired?
data-ng-modelAn array, containing a list of users, in the following format:

var userArray = [{ 'Name': object.Name, 'Title': object.Title, 'Id': object.Id }]

Where, name is a claims based user principal, and Title is the display name of the prinicpal
nullYes (you must supply a model to the data-n-model attribute, but it contain a null value)
data-pp-ready-to-loadtrue | falsefalseYes (the people picker will not render until this value is set to true)
data-pp-is-multiusertrue | falsefalseNo
data-pp-widthAny numerical value, followed by 'px'220pxNo
data-pp-account-typeAny compination of the following values, seperated by a comma;
User,DL,SecGroup,SPGroup
User,DL,SecGroup,SPGroupNo

An example of adding a single user picker;

<div ui-People ng-model="vm.data.su" pp-ready-to-load="{{vm.loadPeoplePickers}}" pp-is-multiuser="{{false}}" pp-width="220px" pp-account-type="User" id="peoplePickerDivAT"></div>

An example of adding a multiple user picker:

<div ui-People ng-model="vm.data.mu" pp-ready-to-load="{{vm.loadPeoplePickers}}" pp-is-multiuser="{{true}}" id="peoplePickerDivAP"></div>

4. Load the data for the model.

Before initialising the directive (via changing the value of the property used in the data-pp-ready-to-load attribute to true), ensure you up the data model used for the people picker with the current data (if any).

For example, you might perform a REST call to get the current values in a list item. You would then create the model for the user picker when data from the REST call is returned.

After updating the data in the people pickers model, you would then change the value of the property used in the data-pp-ready-to-load attribute to true.

The code below shows an example of doing this.

function init() {   
   //Pre-populate the single user field
   //Normally you would get this information from a REST call to Office 365 / SharePoint
   vm.data.su = populatePickerModel({
      Name:'i:0#.f|membership|someone@sometenant.onmicrosoft.com',
      Id:'19', 
      Title:'Matthew Yarlett'});
   vm.loadPeoplePickers = true;
   if (!$scope.$root.$$phase) {
      $scope.$apply();
   }
};

That's it - as far as initialising and using the control goes.

To use the value(s) of a the people picker directive while the page is open, simple reference the people picker model.

E.g.

<p>
    <span data-ng-repeat="r in vm.data.su track by $index">
        <span data-ng-bind-html="vm.getPresence(r.Name, r.Title)"></span>&nbsp;
    </span>
</p>

The example above uses a function on the controller to return the users name including the presence icon, It's just some standard html that I've exported (and slightly tweaked) from a SharePoint user field. Note that it uses a "constweb" variable to get the weburl - you would need to set this!

function getPresence(userId, userTitle) {
   if (userId && userTitle) {
      return '<span class="ms-noWrap"><span class="ms-spimn-presenceLink"><span class="ms-spimn-presenceWrapper ms-imnImg ms-spimn-imgSize-10x10"><img class="ms-spimn-img ms-spimn-presence-disconnected-10x10x32" src="'+constWeb+'/_layouts/15/images/spimn.png?rev=23"  alt="" /></span></span><span class="ms-noWrap ms-imnSpan"><span class="ms-spimn-presenceLink"><img class="ms-hide" src="'+constWeb+'/_layouts/15/images/blank.gif?rev=23"  alt="" /></span><a class="ms-subtleLink" onclick="GoToLinkOrDialogNewWindow(this);return false;" href="'+constWeb+'/_layouts/15/userdisp.aspx?ID=' + userId + '">' + userTitle + '</a></span></span>';
   }
      return '<span></span>';
}

Persisting the value(s) of the a people picker control is just as easy. You use the data in the people pickers model to write back to the server.

In the example below, I have a model used in a REST call for updating a list item that has a people picker field. Data from the people picker directive's model is formatted for the REST call. The ID (SPUser.Id) of each resolved principal (user or group) is added to an array of ID's, which is passed to the server via a REST call.

var sr = sr || {};
sr.models = sr.models || {};

sr.models.listItemModel = function (id) {
   this.Id = id ? id : -1; 
   this.srSingleUserField = null;
   this.srMultipleUserField = {
      results: []
   };   
   this.__metadata = {
      //The type is unique to your list. You can find out the type byte
      //looking at the list using a REST via the browser
      //E.g. http://my.site.com/_api/web/lists/getlistbytitle("YourListName")
      type: 'SP.Data.SrListItem'
   };
}

function populatelistItemModel(srcModel, tagsTerms) {
   var dstModel = new sr.models.listItemModel(srcModel.Id);            
 //Single user field
 if(srcModel.su){
  if(srcModel.su.length == 0){
   dstModel.srSingleUserField = null;
  }
  else{
   var user = srcModel.su[0];
   dstModel.srSingleUserField = user.Id
  }
 } 
   //Mutli user field
   if (srcModel.mu) {
      if(srcModel.mu.length == 0){
         dstModel.srMultipleUserField.results = [];
      }
      else{
         for(var i = 0; i < srcModel.mu.length; i++){
            var user = srcModel.mu[i];
            dstModel.srMultipleUserField.results.push(user.Id);
         }
      }
   }
   dstModel.__metadata.etag = srcModel.__metadata.etag;
   dstModel.__metadata.id = srcModel.__metadata.id;
   dstModel.__metadata.uri = srcModel.__metadata.uri;
   return dstModel;
}

I also added some CSS to override a few of the People Pickers classes, that fixes a few style issues. It's optional, but I've added it below for completeness.

The CSS classes are prepended with div#strk (the parent DIV of my sample), to ensure these classes only affect the app.


/* People Picker Modifications */
div#strk .sp-peoplepicker-autoFillContainer{
 z-index: 20;
 background-color:#fff;
}
div#strk .sp-peoplepicker-topLevel{
 background-color:#fff;
}
div#strk .sp-peoplepicker-topLevel{
 min-height:34px;
}

When it's all said and done, it looks like this;