News

Updated: Apr 1

What you’ll learn in this post:

  1. How to use an angular directive to make a reusable link to add/remove an item to their favorites list.

  2. How to use an angular service to provide reusable client scripting to your widgets.

  3. How to use a scripted REST API when a widget server script isn’t available.

  4. How to include drag’n’drop functionality in your widgets.

The topic of personalizing the portal experience comes up often in my portal design workshops. The best personalized experience is one that features suggestions based on assets assigned to a user via the CMDB. However, many organizations aren’t ready to do that. Therefore, there’s not much we know about the user that allows us to make intelligent personalized suggestions.

Perhaps the next best thing is to allow users to create their own list of favorite content. Using some advanced techniques in AngularJS and Service Portal, we can provide the following features to users.



Save content as favorites from a list.

Users can add and remove favorites from a list of content, such as search results.



Save an item as a favorite from a document.

Users can mark a catalog item as a favorite when viewing the item.




Access favorites from the header menu.

Easy access to favorite content from the header menu.



Manage their favorites via a My Favorites page.

Users can change the order of their favorites by dragging these tiles around.


I demonstrated the full experience and how I built it in a recent Ask the Expert session. I’m not going to explain everything step by step in this post, since you can watch the video and see for yourself.


But as promised, I will share the code and general architecture. Here is everything I used in the video. Enjoy!

Favorites Table

The first thing you’ll need is a table to store your user’s favorites.


The fields I created on this table are:

  1. User (type = reference: sys_user)

  2. Table (type = Table Name)

  3. Document (type = Document ID, dependent on the table field)

  4. Order

Script Include

This script include is used to store server side functions that will be used by many of the other components.


var portalFavorites = Class.create();
portalFavorites.prototype = {
  initialize: function() {
  },
  
  getFavorites: function() {
    var favorites = [];
    
    var fav = new GlideRecord('u_portal_favorite');
    fav.addQuery('u_user', gs.getUserID());
    fav.orderBy('u_order');
    fav.query();
    while(fav.next()) {
      favorites.push(this.formatFavorite(fav));
    }
    
    return favorites;
  },

  isFavorite: function(table, document) {
    var isFavorite = false;

    var fav = new GlideRecord('u_portal_favorite');
    fav.addQuery('u_user', gs.getUserID());
    fav.addQuery('u_table', table);
    fav.addQuery('u_document', document);
    fav.query();
    if(fav.hasNext()) {
      isFavorite = true;
    }
    return isFavorite;
  },

  reorderFavorites: function(newIndex, oldIndex, id) {
    var i;

    var fav = new GlideRecord('u_portal_favorite');
    fav.addQuery('u_user',gs.getUserID());

    if(!oldIndex) {
      //This is a new item, so move everything down in order.
      fav.addQuery('sys_id','!=',id);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i++;
        fav.u_order = i;
        fav.update();
      }
      return;
    }

    if(!id) {
      //An item was removed, so move everything that was behind it up in order.
      fav.addQuery('u_order','>',oldIndex);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i--;
        fav.u_order = i;
        fav.update();
      }
      return;
    }

    if(oldIndex < newIndex) {
      //The item was moved down in order.
      //example, if item moved from 3 to 7, then get the items with indexes 4, 5, 6, & 7 and move them up in order
      fav.addQuery('sys_id','!=',id);
      fav.addQuery('u_order','>',oldIndex);
      fav.addQuery('u_order','<=',newIndex);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i--;
        fav.u_order = i;
        fav.update();
      }
    }

    if(oldIndex > newIndex) {
      //The item was moved up in order.
      //example, if item moved from 7 to 3, then get all items with indexes 3, 4, 5, & 6 and move them down in order
      fav.addQuery('sys_id','!=',id);
      fav.addQuery('u_order','<',oldIndex);
      fav.addQuery('u_order','>=',newIndex);
      fav.query();
      while(fav.next()) {
        i = Math.floor(fav.getValue('u_order'));
        i++;
        fav.u_order = i;
        fav.update();
      }
    }

    //If a favorite was moved (not added or removed), then the last step is to update the new position of the moved item
    var item = new GlideRecord('u_portal_favorite');
    item.get(id);
    item.u_order = newIndex;
    item.update();

  },
  
  removeFavorite: function(id) {
    var fav = new GlideRecord('u_portal_favorite');
    fav.get(id);
    var oldIndex = fav.getValue('u_order');

    if(fav.deleteRecord()) {
      this.reorderFavorites(null, oldIndex);
    }
  },
  
  formatFavorite: function(fav) {
    var item = {};

    item.type = 'favorite';
    item.table = fav.getValue('u_table');
    item.document_id = fav.getValue('u_document');
    item.order = fav.getValue('u_order');
    item.sys_id = fav.getValue('sys_id');
    item.url_target = '_parent';
    
    if(item.table == 'kb_knowledge') {
      item.href = '?id=kb_article&sys_id=' + item.document_id;
      item.label = fav.u_document.short_description.getDisplayValue();
      item.image = fav.u_document.image.getDisplayValue();
      item.icon = 'fa-info';

    } else if(item.table.substring(0, 3) == 'sc_') {
      item.href = '?id=sc_cat_item&sys_id=' + item.document_id;
      
      var catItem = new GlideRecord(fav.getValue("u_table"));
      catItem.get(fav.u_document.sys_id);
      item.label = catItem.getDisplayValue("name");
      item.image = catItem.picture.getDisplayValue();
      item.icon = 'fa-shopping-cart';
        
    }

    return item;
  },

  type: 'portalFavorites'
};


Scripted REST API

I used a scripted REST API to make server side functions available to my directive and service, since Angular Providers don’t have a server script field like widgets do. Note the two resources attached to the API. That’s where this API’s scripts live.


deleteFavorite REST Resource


(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var data = request.body.data;
  var result;

  var fav = new GlideRecord('u_portal_favorite');
  fav.addQuery('u_user', gs.getUserID());
  fav.addQuery('u_table', data.table);
  fav.addQuery('u_document', data.document);
  fav.query();
  if(fav.next()) {
    var oldIndex = fav.getValue('u_order');
    if(fav.deleteRecord()) {
      result = 'success';
      new portalFavorites().reorderFavorites(null, oldIndex);

    } else {
      result = 'fail';

    }
  } else {
    result = 'fail';

  }

  var body = {data: result};

  response.setBody(body);

})(request, response);

insertFavorite REST Resource


(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var data = request.body.data;
  var result;

  var fav = new GlideRecord('u_portal_favorite');
  fav.initialize();
  fav.u_user = gs.getUserID();
  fav.u_table = data.table;
  fav.u_document = data.document;
  fav.u_order = 0;
  if(fav.insert()) {
    result = fav.getUniqueValue();
    new portalFavorites().reorderFavorites(0, null, result);

  } else {
    result = 'fail';

  }

  var body = {data: result};

  response.setBody(body);

})(request, response);


Angular Service

This service is used to provide reusable functions for adding and removing favorites. Since Angular Providers don’t have a server script, like a widget does, I used a scripted REST API to handle the server side functions.


function($http, $q) {

  function _insert(data) {
    var def = $q.defer();
    var putter = '/api/116638/portalfavorites/insertFavorite';
    $http.put(putter, data)
      .success(function(response){
        console.log('insert success');
        def.resolve(response.result.data);
      })
      .error(function() {
        console.log('insert fail');
        def.reject("Failed to get message");
      });

    return def.promise;
  }

  function _delete(data) {
    var def = $q.defer();
    var putter = '/api/116638/portalfavorites/deleteFavorite';
    $http.post(putter, data)
      .success(function(response){
        console.log('delete success');
        def.resolve(response.result.data);
      })
      .error(function() {
        console.log('delete fail');
        def.reject("Failed to get message");
      });

    return def.promise;
  }

  return {

    toggleFavorite: function(item) {
      var data = {
        table: item.type == 'kb' ? 'kb_knowledge' : 'sc_cat_item',
        document: item.sys_id
      };

      if(item.isFavorite) {
        _delete(data).then(function(r){
          item.isFavorite = !item.isFavorite;
        });

      } else {
        _insert(data).then(function(r){
          item.isFavorite = !item.isFavorite;
        });

      }
    }

  };
}


Angular Directive

This directive is used to provide a common HTML template to be used as the toggle link. Notice that we reference the above service in two ways:

  1. Attach it to the directive file via the Required Providers related list.

  2. Inject the service into the directive’s function by name.


function favoriteToggle(portalFavorites) {
  return {
    restrict: 'E',
    scope: {
      item: "="
    },
    replace: true,
    link: function(scope) {
      scope.favorites = portalFavorites;
    },
    template: '<div class="ht-portal-favorite-toggle">' +
      '<a href="javascript:void(0)" ng-click="favorites.toggleFavorite(item)">' +
      '<i class="fa" ng-class="item.isFavorite ? \'fa-heart\' : \'fa-heart-o\'"></i>' +
      '</a>' +
      '</div>'
  };
}

My Search Page Widget

The components documented above are what we need to add the favorite toggle icon link to any widget. You will need to create custom copies of any out-of-box widget with which you plan to enable favorites. Don’t forget to attach the “favoriteToggle” directive to your widget via the Angular Providers related list.

The first step in enabling your widget is to add the directive to the HTML template. I added it to line 30, inside the existing ng-repeat.

<div role="listitem" ng-repeat="item in data.results | orderBy:'score':true | limitTo:data.limit" class="panel-body b-b ">
  <favorite-toggle item="item"></favorite-toggle>
  <div ng-include="item.templateID"></div>
</div>

Notice how we are passing the “item” object to the directive via the “item” attribute? We need to update the item object so that it stores information our supporting scripts will find useful for updating the item in the favorites table. In the case of the Search Page widget, we need to go to the portal’s Search Sources to do that.

Search Sources

The out-of-box portal uses two search sources that we’re interested in for this example: Knowledge Base and Service Catalog.


These search sources already provide most of the information we need to know about the item, namely “sys_id” and “type”. We just want to know one more thing about the item. Is it already a favorite? To find out, I add the following property definition to the “Data Fetch Script” field for each search source:

KB search source – line 30:

article.isFavorite = new portalFavorites().isFavorite('kb_knowledge', kb.getUniqueValue());

SC search source – line 39:

 item.isFavorite = new portalFavorites().isFavorite('sc_cat_item', item.sys_id);

Now the item objects that come from each search source will have a property, isFavorite, that we can use to show the right icon depending on the existing favorite status of each item.

My KB Article Page & My SC Catalog Item Widgets

We need to do similar work for the document detail widgets as we did for the search page widget. First clone the out-of-box widgets KB Article Page and SC Catalog Item and put them on their respective pages. The processing scripts we wrote are expecting to receive an object containing (at least) three properties: sys_id, type, and isFavorite. The server scripts of these widgets only define one of those properties, sys_id. Therefore, we need to define an item object that we can give to the directive.

KB Article Page server script, line 41:

data.item = {
  sys_id: data.sys_id,
  type: 'kb',
  isFavorite: new portalFavorites().isFavorite('kb_knowledge', t.sys_id)
};

KB Article Page HTML template, line 8:

 <favorite-toggle item="data.item"></favorite-toggle>

SC Catalog Item server script, line 124:

data.item = {
  sys_id: data.sys_id,
  type: 'sc',
  isFavorite: new portalFavorites().isFavorite('sc_cat_item', data.sys_id)
};

SC Catalog Item HTML template, line 15:

<favorite-toggle item="data.item"></favorite-toggle>

Manage Favorites Widget

This widget allows users to reorder their favorites, remove items, and access the articles and items. It can be used on any page, and is an ideal candidate for the homepage. However, I made a new page with the ID “favorites”.

To support the drag’n’drop feature of this widget, you’ll need to include the Sortable javascript library.


URL:  https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js

HTML Template

<div class="ht-manage-favorites-container">

  <h3>
    ${Manage Favorites}
  </h3>

  <div>
    <p>
      ${Use this page to manage your favorite items.} <br />${Reorder them by dragging them around. Remove them with the 'remove' button.}
    </p>
    <h5 ng-if="c.Favorites.items.length < 1">
      ${There are no favorites yet. Save items as favorites by clicking on the heart in lists and form/article headers.}
    </h5>
  </div>

  <div id="favorites" class="ht-favorites">
    <div class="ht-favorite ht-card" document_id="{{item.document_id}}" id="{{item.sys_id}}" ng-repeat="item in data.items">
      <div class="ht-card-image">
        <img ng-if="item.image" class="ht-card-img" ng-src="{{item.image}}" alt="Card image cap">
        <div ng-if="!item.image" class="ht-card-icon">
          <i class="fa {{item.icon}}"></i>
        </div>
      </div>
      <div class="ht-card-body">
        <h4 class="ht-card-title">{{item.label}}</h4>
        <div class="ht-card-buttons">
          <a ng-href="{{item.href}}" class="btn btn-primary"><i class="fa fa-search"></i> ${View}</a>
          <a href="javascript:void(0)" class="ht-remove btn btn-danger"><i class="fa fa-close"></i> ${Remove}</a>
        </div>
      </div>
      <div class="ht-favorite-loading-icon" ng-show="item.saving">
        <i class="fa fa-spinner fa-spin" ></i> 
      </div>

    </div> 
  </div>

</div>

Client Script

function($scope, $rootScope, spUtil) {
  var c = this;

  var el = document.getElementById('favorites');
  var sortable = Sortable.create(el, {

    onSort: function(evt) {

      var payload = {
        action: 'sort', 
        item: evt.item.id, 
        newIndex: evt.newIndex, 
        oldIndex: evt.oldIndex
      };

      c.server.get(payload);

    },

    filter: '.ht-remove',
    onFilter: function (evt) {

      //Expose the spinny icon for the item being removed
      c.data.items.some(function(e){
        if(e.sys_id == evt.item.id) {
          e.saving = true; 
        }
      });

      var payload = {
        action: 'remove', 
        item: evt.item.id
      };

      c.server.get(payload).then(function(){
        for(var f=0; f<c.data.items.length; f++) {
          if(c.data.items[f].sys_id == payload.item) {
            c.data.items.splice(f,1);
            break;
          } 
        }
      });

    },

    handle: '.ht-favorite',
    draggable: '.ht-favorite',
    animation: 150

  });

}

Server Script

(function() {

  var favorites = new portalFavorites(); //A reference to the script include

  data.items = favorites.getFavorites();

  if(input.action == 'sort') {
    favorites.reorderFavorites(input.newIndex.toString(), input.oldIndex.toString(), input.item); 
  }

  if(input.action == 'remove') {
    favorites.removeFavorite(input.item);
  }

})();

CSS

A little CSS to pull it all together. Remember to add this style sheet to your theme, and give your browser a hard refresh to see the results. Note, everything in this style sheet could go in the CSS field of the Manage Favorites widget, EXCEPT FOR the first selector object, .ht-porta-favorite-toggle. That one definitely needs to be in a portal wide style sheet since that class name comes from a directive that is used by multiple widgets.

.ht-portal-favorite-toggle {
  float: right;
  font-size: 1.5em;
}
.ht-manage-favorites-container {
  min-height: 500px; 
}
.ht-favorites {
  display: grid;
  justify-content: center;
  grid-auto-rows: auto;
  grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
  grid-gap: 2rem 2rem;
}
.ht-favorite {
  min-height: 275px;
  margin-bottom: 2rem;
  background: #fff;
  position: relative;
}
.ht-card {
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  transition: 0.3s;
  text-align: center;
}
.ht-card:hover {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
.ht-card-body {
  padding: 2px 16px 12px;
}
.ht-card-image {
  height: 150px;
}
.ht-card-image img{
  height: 100%;
}
.ht-card-icon i {
  font-size: 72px;
  margin-top: 36px;
  background: #666;
  padding: 12px;
  width: 100px;
  border-radius: 50px;
  color: #ededed;
}
.ht-card-body .btn {
  font-size: 1.2rem;
  border-width: 3px;
}
.ht-card-buttons {
  position: absolute;
  left: 0;
  right: 0;
  text-align: center;
  bottom: 1rem;
}
.ht-card-buttons a:last-child {
  margin-left: 1rem;
}
.ht-favorite-loading-icon {
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0;
  left: 0;
}
.ht-favorite-loading-icon i {
  font-size: 72px;
  color: #f0f0f0;
  margin-top: 48px;
  opacity: .5;
}

Scripted Menu Item

By using native service portal menu functionality, we have a lot of control over what items appear in our menus. By following the example set forth by the My Requests menu item, we can easily create a My Favorites menu item. But as you can see, the script for favorites is much simpler.


// maximum number of entries in this Menu
var max = 30;

var t = data; // shortcut
t.items = new portalFavorites().getFavorites();

t.items = t.items.slice(0, max); // only want first 30
t.count = t.items.length;

var link = {title: gs.getMessage('Manage My Favorites'), type: 'link', href: '?id=favorites', items: []};
t.items.unshift(link); // put 'Manage My Favorites' first

t.record_watchers = [];
t.record_watchers.push({'table':'u_portal_favorite','filter':'u_user='+gs.getUserID()});

In Conclusion

As you can see, there are a lot of moving parts to this. Here’s a quick review of the components we built:

  1. Table: u_portal_favorite

  2. Script Include: portalFavorites

  3. Scripted REST API: portalFavorites

  4. Resource: deleteFavorite

  5. Resource: insertFavorite

  6. Angular Provider Service: portalFavorites

  7. Angular Provider Directive: favoriteToggle

  8. Widgets:

  9. My Search Page

  10. My KB Article Page

  11. My SC Catalog Item

  12. Dependency: sortable

  13. CSS: favorites.css

  14. Scripted Menu Item: My Favorites

I limited my example to three widgets and a menu. But you can go wild and include this functionality wherever you like, for any other kind of content too.

Note:

One thing I’d like to point out is that the record watch in our scripted menu item doesn’t get along too well with our Manage Favorites widget. The problem occurs when a user has a significant number of favorites (more than 5 for example), and then they reorder the items. This results in a several GlideRecord updates, since we need to update the order field on all those favorites. Subsequently, there is a noticeable ‘loading’ lag while the the record watch in the menu item processes each update.

There are a few solutions for this:

  1. Don’t use the record watch in the favorites menu.

  2. Create a separate favorites menu widget which allows you more control over how the record watch is processed. You can watch for selective table activity, and respond to it in ways that don’t result in a noticable quirk.

  3. Use a different table approach. Instead of having a record for each favorite item, maintain only one record for each user. Then use a single text field that stores the favorites data in JSON format. You’ll obviously need to modify the way you read/write the favorites in processing scripts we detailed above.

However you choose to approach this concept of favorite content, hopefully you know have some new ideas and skills to implement your own version.

Aloha!

Jeff

#portalguru #serviceportal #servicenow #widgets

Perhaps the most important skill to have as a service portal widget developer is the ability to create reusable script components. Some call it cleanliness, some call it poetry… but whatever you call it, developing easily reusable scripts results in more fun coding. Furthermore, it leads to scalability and stability in your work.

In this post, you will learn how to:

  1. Use Scripted REST API in an Angular Provider service script

  2. Use deferred promises with the $http and $q services for asynchronous REST API calls

  3. Modularize a reusable message retriever for dynamic message translations

  4. Pull it all together in a Service Portal widget

Perhaps you joined us for Cerna’s first live podcast on July 20. In it, Will Smith, Tanner Kibler, and I demonstrated a Service Portal method for retrieving dynamic message translations. It builds on a concept outlined by Chuck Tomasi in a community post, where he showed us how to use the second parameter of gs.getMessage().

See Chuck’s post here.

As you may know, the second parameter method only works in a server side script. That in itself isn’t a terrible limitation. Especially considering it is easy to define the message server side and pass it to the client controller script. Ie:

Server Script:

var values =[‘Some Value’, ‘Another Value’];
data.message = gs.getMessage(‘someMessageKey’, values);

Client Script:

c.message = data.message;

However, this could involve a bit of complexity if you need to start the message definition client side. You’ll need to request an update from the server, then wait for the response to reach the client script. While that’s not awful, it’s not ideal if you have lots of custom widgets to translate. Consequently, you will end up repeating this code over and over.

For example:

Client Script:

var values =[‘Some Value’, ‘Another Value’];
c.server.get({action: ‘getMessage’, key: c.someMessageKey, values: values})
   .then(function(response){
      c.message = response.data.message;
   });



Server Script:

if (input && input.action == ‘getMessage’) {
   data.message = gs.getMessage(input.key, input.values);
}



I strongly dislike repetitive code. Hence, I set my mind to developing a more modular solution that didn’t require requesting a server script update. As a result, my team collaborated on a reusable pattern. Here it is for you to learn from.

Note: Maybe message translation isn’t the use case you are trying to solve for. Regardless, the pattern demonstrated here can be used in any scenario that requires teamwork between client side and server side. All scripts are repeated at the end of the post for copy/paste purposes.


Step 1: The Scripted REST API

First, we create a scripted REST API. All we do at the header level is give the API its name.


Next, create a REST resource that will accept parameters from whatever script is calling it. This does the actual message translation, and returns it to the requesting script via the response body.


Step 2: Angular Provider Service

You’ll want a reusable function that will communicate with the REST API. This angular provider can be injected into any widget controller. With this, we can pass to the REST API all the required parameters for message translation. The REST API takes a small moment to process, therefore we use the AngularJS $q service to handle the function promise. Otherwise, our client controller won’t wait for us, resulting in an undefined value.


Step 3: Widget Client Controller

The widget’s client controller invokes the provider service’s getDynamicMessage function. Remember, you have to attach this to the widget via related list, in addition to injecting it by name. The important part of this script is the getDynamicMessage call. The other stuff is simply to support the demo.


Step 4: HTML Template

We need a way to collect message parameters. I’ve created a simple form with several inputs to demonstrate this.


Step 5: UI Message

Finally, we create the message itself. The first parameter sent to the REST API is our message key. The second is an array of values that can be injected by index number. As a result, we can construct translations that support the grammar rules of any language.


The Result

Let’s see it in action, starting with a blank form.


The user tells us their name and favorite topping. Then they choose their dish.


The final result is the dynamically built confirmation message.


In Summary

The reusable parts of this whole thing are the scripted REST API and the angular provider service. The usage is quite easy. From any widget controller (that injects the portalMessages service), simply call the ‘getDynamicMessage’ function provided by the ‘portalMessages’ service.

portalMessages.getDynamicMessage('messageKey', ['some value', 'some other value'])
   .then(function(message){                                         
      //Do something with your translated message
   });

Seems like a lot going on here. So I’ll summarize the whole process again in a nutshell:

  1. The HTML template takes inputs from the user.

  2. The client controller passes these inputs to the angular provider service.

  3. The angular provider passes the inputs to the scripted REST API.

  4. The REST API translates and returns the message.

  5. The angular provider service returns message to the client controller.

  6. The client controller patiently waits for the message.

  7. The HTML template is updated.

As you can see, it’s not always necessary to use the server script of a widget. This is particularly true when you have a common evaluation that might get used in any given widget.

Hopefully I’ve helped nudge you a step closer to service portal widget mastery, now that you understand how to use REST API in tandem with angular providers.

Aloha!

Text versions of the scripts for your copy/paste pleasure:

Scripted REST API Resource

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

   //Data is received from the Angular Provider service that makes the REST call
   var data = request.body.data;

   //Get the message translation
   var message = gs.getMessage(data.key, data.values);

   //Prepare the response body
   var body = {data: message};

   //Set the response body
   response.setBody(body);

})(request, response);


Angular Provider Service

function($http, $q) {
   //$http service to enable the REST API call
   //$q service to handle deferred promises to aid with asynchronous message retrieval

   return {

      getDynamicMessage: function(key, values) {
         //key and values are received from the client controoler

         //Enable a deferred promise
         var def = $q.defer();

         //This object gets sent to the scripted REST API
         var data = {
            key: key,
            values: values
         };

         //Set up the REST API call
         var messageGetter = '/api/116638/myportalmessages/getdynamicmessage';
         $http.post(messageGetter, data)
            .success(function(response){
               def.resolve(response.result.data);
            })
            .error(function() {
              def.reject("Failed to get message");
            });

         return def.promise;
      }
   };
}


Widget Client Controller

function(portalMessages, spModal) {
   var c = this;

   c.orderFood = function(food) {
      portalMessages.getDynamicMessage('confirmationMessage', [c.name, c.topping, food])
         .then(function(message){
            c.confirm(message); 
         });
   };

   c.confirm = function(message) {
      spModal.open({
         size: 'lg',
         title: '${confirmationModalTitle}',
         message: message,
         buttons: [
            {label:'${Ok}', primary: true}
         ]
      });
   };

}


Widget HTML Template

<form>

   <div class="form-group">
      <label for="nameInput">${Your name}</label>
      <input ng-model="c.name" type="text" class="form-control" id="nameInput" aria-describedby="nameHelp" placeholder="${ie, Fred}">
   </div>
   <div class="form-group">
      <label for="toppingInput">${Your favorite topping}</label>
      <input ng-model="c.topping" type="text" class="form-control" id="toppingInput" aria-describedby="toppingHelp" placeholder="${ie, cheese}">
   </div>

   <div class="btn-group" role="group">
      <button type="button" class="btn btn-primary" ng-click="c.orderFood('pizza')">${Order Pizza}</button>
      <button type="button" class="btn btn-primary" ng-click="c.orderFood('taco')">${Order Taco}</button>
      <button type="button" class="btn btn-primary" ng-click="c.orderFood('curry')">${Order Curry}</button>
   </div>

</form>

#restapi #widget #serviceportal #angularproviders #angularjs


The wait is over for those of you longing for the scripts I used in the Angular Providers webinar on May 1. I apologize for the delay, although I do have excuses:

  1. I was completely consumed over the last two weeks with debuting our brand new Haiku Theory service portal accelerator at K18. Stay tuned for more info on that!

  2. While at K18, the SN dev instance I used for the webinar was ‘reclaimed’, as if it was going to make a nice coffee table or something.

Here’s the link to the webinar, in case you haven’t seen it, or want to watch it over and over. SN Ask the Experts: Understanding Angular Providers

Without further ado, the scripts:

SERVICE

A service can be used to share scripts and data across the client side of widgets. Multiple widgets can all create references to objects initialized by the service, therefore many widgets can share the same data and functions.

module: Angular Providers


For your copy/paste pleasure:

//Inject whatever services you want to use
function($location) {

  //Initialize some properties if you like
  var myData = {};
 
  return {
    //These functions, can be accessed by any widget that injects this service into its controller script

    getMyData: function() {
      return myData;
    },

    someFunction: function(input) {
      //Do some stuff. Really go crazy man.
      var someStuff = !input;
      $location.get('stuff', someStuff);

      return someStuff;
    }
  };

}

WIDGET CONTROLLER

To include the above service in a widget, simply inject it into the controller function by name. Then remember to add this service to the widget’s Angular Providers related list.

module: Widgets


See how I make use of the service’s functions and make local references to them? Very handy.

Copypasta:

function(myMenuService) {
  var c = this;

  //Map a local property to your service
  c.myMenu = myMenuService;
  
  //Call the service's functions to reference objects and do stuff
  c.myData = c.myMenu.getMyData();
  c.someValue = c.myMenu.someFunction(someInput);
}

DIRECTIVE

Now say that you wanted to enhance your widget’s HTML with powerful mini templates. This example directive will create an HTML element with a link that calls a function provided by the directive’s own link function.

Just like the service, be sure to add this to your widget’s Angular Providers related list.

module: Angular Providers


C/P:

function myMenuItem() {
  return {
    restrict: 'E',
      /*
      controls how you intend to reference (call) the directive
      A: by attribute: <div your-directive></div>
      E: by element name: <your-directive></your-directive>
      C: by class name: <div class="your-directive"></div>
      M: by comment <!-- directive: your-directive -->
      */
    scope: true,
      /*
      false: Use the parent's scope. Making changes to the scope in the directive will update the scope of the widget.
      true: Clone the parent's scope. The directive gets a copy of the parent's scope, but making changes to the scope in the directive will not be reflected in the parent's scope.
      {}: By providing an object you isolate the scope, meaning this directive has it's own empty scope, not related to the parent's scope. You can define the directive's scope via attribute:
        Example - scope: { =someProperty }
          In this case, someProperty gets passed through to this directive via the directive's attribute:
          <my-menu-item some-property="someValue"></my-menu-item>
        There are many ways to do this. Google it for more examples and info.
      */
    replace: true,
      /*
      true: the entire HTML element that references this directive will be replaced by this template
      */
    link: function(scope) {
      /*
      Provide your own client controller for this directive.
      Inject the parent's scope and get access to everything the widget does.
      */
      scope.toggleSomething = function(item) {
        scope.server.get({'action': 'toggle', 'item': item}).then(function(response){
          item.property = !item.property; 
        });
      };
    },
    template: '<div>' +
      '<a href="javascript:void(0)" ng-click="toggleSomething(item)">{{item.label}}</a>' +
    '</div>',
      /*
      Write your HTML in string form.
      Be sure to use \ to escape apostrophes inside your strings.
      Directives can have only one parent HTML element.
      */
    templateURL: 'someTemplateURL.html'
      /*
      Or provide a template in the sp_ng_template table. That's much easier than writing the HTML in script form, but remember to attach them to your widget via the Templates related list.
      */
 };
}

This directive is used in your widget’s HTML just like this:

<my-menu-item ng-repeat="item in c.Items"></my-menu-item>

FILTER MODULE

I also showed you an example of how to use a filter module. The filter I showed would remove duplicate entries from an array when using ng-repeat.

module: UI Scripts


Copypasta:

(function() {
  angular.module('myFilter', []).filter('myUniqueFilter', function() {
    //This filter will remove any duplicates from the input array based on the provided filterBy property
     // *input* is the array of objects being iterated.
     // *filterBy* is the name of the property you want to filter by

    //Use this in your HTML template like this:
     //<div ng-repeat=" item in c.Items | uniqueFilter : 'label' " >{{ item.label }}</div>
    
    //This is a UI Script included in the portal via portal dependencies

    return function(input, filterBy) {
      if(input) {
        if (filterBy) {
          var output = [];
          //start with an empty output array
          for (var i = 0; i < input.length; i++) {
            //for each item in the input array, see if there's a matching item in the output
            var unique = true;
            for (var o = 0; o < output.length; o++) {
              //Look for an existing item in the output array.
              if(output[o][filterBy] === input[i][filterBy]) {
                unique = false;
              }
            }
            if(unique) {
              //If item doesn't already exist in the output array, put it there
              output.push(input[i]);
            }
          }
          return output;
        } else {
          //If no filterBy property has been provided, return the entire input array
          return input;
        }
      } else {
        console.log('there is no input');
        input = [];
        return input;
      }
    };
  });
})();

To use this, first add this script as a dependency to your widget via the Dependencies related list.

module: Dependencies


Now you can reference the filter in an ng-repeat expression:

<div ng-repeat=" item in c.Items | myUniqueFilter : 'label' " >{{ item.label }}</div>

BONUS

For sticking around until the end, here’s a bonus. This is me debuting Haiku Theory at K18 last week in Las Vegas. Can’t wait to share more info about that in the coming weeks.

Aloha, Jeff

Knowledge18, Las Vegas


Start Now

Security & Risk Solutions
IT Solutions
Business Solutions
HR Solutions
Customer Solutions
CS 2020 LOGO - solutions tagline (white)

Email:    info@cernasolutions.com

Phone:  +1 844 804 6111 (US)

               +44 (20) 33254077 (UK)

  • White LinkedIn Icon
  • White YouTube Icon
  • White Twitter Icon
Company
Insight
Products
ServiceNow Services

© 2020 Cerna Solutions, Inc. All Rights Reserved. 2056 Palomar Airport Road Carlsbad, CA, 92011.