Gridx in XPages – 10: Nested Sorting

In the last few posts, we looked at implementing column sorting on remote and local data stores along with additional sorting features. In this post, I’ll show how to simply implement nested column sorting.

Gridx Series

Gridx in XPages — Entire Series

The NestedSort Module

The NestedSort module is a great feature of Gridx – all you have to do is add the module to the grid an it will automatically provide the ability to dynamically sort by multiple columns!

All you have to do is add the NestedSort module to the require statement…

require([
  ...
  "gridx/modules/NestedSort",
  ...
], function(Grid, MemoryStore, Cache, Resizer, NestedSort) {

… and then add it to the grid.

grid = new Grid({
  ...
  structure: columns,
  modules: [
    Resizer,
    NestedSort
  ]
});

That’s it!

Once you sort one column and then hover over another column header, you see a 1 in the first sorted column and then you see a 2 in the next column, along with two arrows. The first one will nest this as the second-level sort. The second arrow will remove the existing sort and instead this column.

Gridx 10a - second sort options

When you have two columns sorted, you’ll see arrows indicating the direction and numbers indicating the sort priority.

Gridx 10b - nested sort

When you hover over a column that’s sorted descending, you’ll see an ‘x’ icon that allows you to clear that sort level.

Gridx 10c - nested sort clear or single sort

This is great functionality for virtually no effort!

Data Store Consideration

This works simply on a local data store (which can still be pulled from the server) — to work with a remote data store, the server would have to handle the sorting and provide the data in the proper order.

Live Demo

To test this out for yourself, check out the demo page.

Gridx in XPages – 9: More Sorting Features

In the last two posts, we looked at implementing column sorting on remote and local data stores. In this post, we’ll look at more available sorting functionality: setting the initial sort order, sorting the grid programmatically, preventing column sorting, and creating your own custom sorting function.

Gridx Series

Gridx in XPages — Entire Series

Setting the Initial Sort Order

By default, the data will be displayed in whatever order it is sent by the REST service, but when you implement the SingleSort module, you can add the sortInitialOrder property to the grid to set the default sort order.

All you have to do is add the attribute when setting up the grid object. It takes two parameters: colId and descending. The column ID will match the id property of the column definition — not necessarily the name of the column in the underlying view (unless you define it that way).

grid = new Grid({
  id: "my_gridX",
  ...
  sortInitialOrder: {colId: 'last', descending: false},
  ...

Programmatic Column Sorting

Another nice feature that the SingleSort module provides is the ability to programmatically sort (or un-sort) the grid.

Here are a few examples…

You can sort a column based on its ID. This line will sort the state column ascending. The parameter to the sort() function is true for descending order and false for ascending.)

grid.column('state').sort(false);

You can also sort a column by its (zero-based) position in the grid. This line will sort the second column in the grid descending.

grid.column(1).sort(true);

The API also provides the ability to clear the current sort — something you cannot do by clicking on the grid.

grid.sort.clear();

Note: In all these cases, grid is the name of the variable that references the grid object.

Live Demo

This demo page includes buttons to programmatically sort (and un-sort) the grid as well as a property to set the initial grid sort by last name.

Preventing Column Sorting

If you want to enable column sorting but selectively prevent it from one or more columns, you can simply do so by setting the column’s sortable attribute to false.

You definitely want to do this if you’re using a remote data store and the underlying view does not support resorting by a given column, because it would make the request, reload the data (in the same order as before) and indicate that it’s sorted, even though it’s not.

This line prevents the firstname column from being sorted:

var columns = [
  {id: 'first', field: 'firstname', name: 'First', width: '65px', sortable: false}
]

Defining a Custom Sort Algorithm

Sorting will be performed with string comparisons by default. If you want to define your own custom sorting logic, you need to do two things:

  1. Add the FormatSort module extension to the grid
  2. Define a comparator function for that column

Here is the full source code for my test page, which includes buttons to sort the grid programmatically as well as a custom sort order (comparator) function for the firstname column.

Lines 6-8 are buttons that programmatically sort (or un-sort) the grid.

Line 19 includes the FormatSort model extension that the grid requires for custom sorting and lines 66-68 add it to the grid. (It’s a model extension, so does not get included via the modules attribute.)

Lines 25-36 define the custom sorting function for the firstname column. In a comparator function, return a positive value when the first value is greater than the second value, a negative value when the first value is less than the second value, or 0 if they are equal.

In this case, I’m demonstrating a handy function that will sort all people with the first name Brad to the top of the list and all people named Marky to the bottom of the list. (It could be written more concisely, but I wanted to make sure the logic is clear.)

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xc="http://www.ibm.com/xsp/custom">

<xc:ccGridxResources></xc:ccGridxResources>

<input type="button" value="Sort by State Asc" onClick="grid.column('state').sort(false);"></input>
<input type="button" value="Sort by column(1) [Last Name] Desc" onClick="grid.column(1).sort(true);"></input>
<input type="button" value="Clear Sort" onClick="grid.sort.clear();"></input>

<div id="gridX_Here" style="height:300px; width:430px;"></div>
          
<script>
require([
  "gridx/Grid",
  "dojo/store/Memory",
  "gridx/core/model/cache/Sync",
  "gridx/modules/ColumnResizer",
  "gridx/modules/SingleSort",
  "gridx/core/model/extensions/FormatSort",
  "dojo/domReady!"
], function(Grid, MemoryStore, Cache, Resizer, SingleSort, FormatSort) {

  var columns = [
    {id: 'first', field: 'firstname', name: 'First', width: '65px', sortable: true,
      comparator: function(name1, name2) {
        if (name1 == 'Brad') {
          return -1;
        } else if (name2 == 'Brad') {
          return 1;
        } else if (name1 == 'Marky') {
          return 1
        } else if (name2 == 'Marky') {
          return -1;
        } else {
          return name1 > name2;
        }
      }  
    },
    {id: 'last', field: 'lastname', name: 'Last', width:'150px'},
    {id: 'state', field: 'state', name: 'State', width: '35px'}
  ];


  // Make an AJAX call to look up the full data set and store it locally for fast access
  dojo.xhr.get({
    url:"X_REST.xsp/gridData_LocalStore",
    handleAs:"json",
    load: function(restData){
    
      // Load the data into a local memory store
      var store = new MemoryStore({
        data: restData,
        idProperty: '@noteid'
      });
    
      grid = new Grid({
        id: "my_gridX",
        cacheClass: Cache,
        store: store,
        structure: columns,
        sortInitialOrder: {colId: 'last', descending: false},
        modules: [
        	Resizer,
        	SingleSort
        ],
        modelExtensions: [
          FormatSort
        ]
      });

      //Put it into the DOM tree.
      grid.placeAt('gridX_Here');
      grid.startup();
    
    },
    error: function(msg, args) {
      console.error('Error loading grid data');
      alert('There was an error loading the data'); 
    }
  });

  });
</script>
</xp:view>

Another use case for a custom sort would be if you have dates stored as strings and need to sort them. You can write a function to turn them into Date objects and then compare them. If you’re using a REST service providing a NotesDateTime object, it will be provided in a format that will automatically sort properly without the custom comparator.

Gridx in XPages – 8: Column Sorting with a Local Data Store

In the last post, I showed how to implement column sorting in a Gridx grid with using a remote data store (JsonRest). In this post, I’ll show how to use AJAX to pull all the data locally for faster (and simpler) sorting.

Gridx Series

Gridx in XPages — Entire Series

SingleSort

The SingleSort module provides the ability to click on column headers and sort the data. The first click on a column header sorts the data ascending and the second click sorts descending.

When the data is stored locally, all you have to do is add the SingleSort module to the grid and it will handle the sorting automatically. (As opposed to the remote storage where the REST service has to provide the data in sorted order and the underlying view has to support the sort order.)

Making Remote Data Local

Of course, the grid wouldn’t be very useful if we could only display data that was available locally. But storing the data locally makes manipulation (such as sorting) much faster, so it’s worth investigating whether that’s a better fit for your application.

Do get remote data locally, we can use a Dojo AJAX request (xhr.get()) to retrieve data from the REST service. After the data has been retrieved, we can use the load() method to set up our local data store and initialize the grid.

Working Example

Here is the entire source of an XPage that performs a remote lookup via AJAX and implements column sorting.

Line 4 loads the common gridx resources as described in this post.

Line 6 defines where the grid will be placed.

Lines 9-16 include the required modules for the data store and the grid. Note that the data store and the cache module are different when using local storage than remote storage (via JsonRest). Local storage uses a Dojo Memory store and syncrhonous cache, while remote storage uses a JsonRest store an an asynchronous cache.

Lines 25-27 make the AJAX call to the REST service to retrieve the data.

Line 28 defines the load method, which will run after the data has been retrieved. (The rest of the code needs to wait until the data is available.)

Lines 31-34 set up the local Memory store for the data. It uses the data retrieved from the REST service and sets the idProperty attribute to define the column storing a unique ID for each row.

Lines 36-45 initialize the grid. This code is exactly the same as the previous example.

Lines 51-53 define the error handler for the AJAX call. If something went wrong with retrieving the data, then this method would run. It should be used to display an error message.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xc="http://www.ibm.com/xsp/custom">

<xc:ccGridxResources></xc:ccGridxResources>

<div id="gridX_Here" style="height:300px; width:300px;"></div>
          
<script>
require([
  "gridx/Grid",
  "dojo/store/Memory",
  "gridx/core/model/cache/Sync",
  "gridx/modules/ColumnResizer",
  "gridx/modules/SingleSort",
  "dojo/domReady!"
], function(Grid, MemoryStore, Cache, Resizer, SingleSort) {

  var columns = [
    {id: 'first', field: 'firstname', name: 'First', width:	'70px'},
    {id: 'last', field: 'lastname', name: 'Last'},
    {id: 'state', field: 'state', name: 'State', width: '40px'}
  ];

  // Make an AJAX call to look up the full data set and store it locally for fast access
  dojo.xhr.get({
    url:"X_REST.xsp/gridData_LocalStore",
    handleAs:"json",
    load: function(restData){
    
      // Load the data into a local memory store
      var store = new MemoryStore({
        data: restData,
        idProperty: '@noteid'
      });
    
      grid = new Grid({
        id: "my_gridX",
        cacheClass: Cache,
        store: store,
        structure: columns,
        modules: [
        	Resizer,
        	SingleSort
        ]
      });

      //Put it into the DOM tree.
      grid.placeAt('gridX_Here');
      grid.startup();
    },
    error: function(msg, args) {
      console.error('Error loading grid data');
    }
  });
});
</script>
</xp:view>

Live Demo

If you’d like to see this in action, I added a new section to my demo database.

There is a page with column sorting with a remote data source and a page with column sorting on a local data source. Both examples work with 1000 rows of data.

I’ll add more to the demo database as I write about additional grid features.

Gridx in XPages – 7: Column Sorting with a Remote Data Source

In the last post, we saw how to add a feature module to Gridx, using the simple example of resizing columns. In this post, I’ll show how to add column sorting with a grid using a remote data source.

Gridx Series

Gridx in XPages — Entire Series

SingleSort

The SingleSort module provides the ability to click on column headers and sort the data. The first click on a column header sorts the data ascending and the second click sorts descending.

Sort of. (See what I did there?)

If the data source is remote — which has been the case in our examples using the JsonRest data store to read live data remotely from a REST service — then the sorting is left up to the server. Just enabling column sorting on the view underlying our data isn’t sufficient — the REST service is responsible for returning the data in the proper order.

Without this, each click on a column header will execute a request to the server to retrieve the data — but it will come back in the same order in which it started. Even worse, the column header will display an arrow indicating the direction that it expects that the data is sorted.

The Sort Request

Watching the Net panel in the browser’s developer tools, I can see that a click on a column header generates a GET request to the REST service, adding a query string like this: ?sort(+lastname). This indicates that it is requesting that the data be sorted ascending by lastname. There is a minus sign (-) in place of the plus sign (+) when it is requesting a descending sort.

Gridx 7 - Net Panel Sort Requests

Sorting the REST Service

Fortunately, the REST service has two properties that we can compute in order to support this functionality (under basics > service > ): sortColumn and sortOrder

The sortColumn property can be computed to read the column name out of the query string. The value sent will be the programmatic column name, so it will correspond to a column in the view that’s providing the data to the REST service. (If you’re not using a view to provide the data, then you’ll need to provide your own sort routine in the code.)

This code will compute the column name by parsing it out of the URL:

var sortColumn = '';
var qs = context.getUrl().getQueryString();

if (qs.indexOf('sort') > -1) {
  // Set the starting point based on where it finds 'sort', then add 4 for those letters, then 2 more for the open parenthesis and the plus or minus sign
  var start = qs.indexOf('sort') + 6;
  var end = qs.indexOf(')', start);
  var sortColumn = qs.substring(start, end);
}
return sortColumn;

Similarly, the sortOrder property can read the sign out of the query string and return ‘ascending’ or ‘descending’ as needed.

var sortDirection = '';
var qs = context.getUrl().getQueryString();
if (qs.indexOf('sort') > -1) {
  // Set the starting point based on where it finds 'sort', then add 4 for those letters, then 1 more for the open parenthesis
  var start = qs.indexOf('sort') + 5;
  var sortSign = qs.substr(start, 1);
  sortDirection = (sortSign == '-') ? 'descending' : 'ascending';
}
return sortDirection;

Interestingly, the plus sign (+) is not returned as part of the query string. I assume that it’s treated as a placeholder for a space as it does when encoding a URL. Fortunately, the minus sign (-) remains, so we can just check for that and otherwise assume ascending.

Important – The View Must Support the Sort

The underlying view must support column sorting in each direction for this to work. You can set the properties of each column in the view that needs to provide sorting via the Click on column header to sort property. This will cause the view to include an index for one or both sort directions for that column.

Gridx 7 - Column Properties

Updated Grid Code

The updates to the REST service required much more effort than the updates required for the grid itself. All we have to do is include the SingleSort module and add it to the grid object.

Here’s the updated module loading snippet:

  require([
    "gridx/Grid",
    "gridx/modules/ColumnResizer",
    "gridx/modules/SingleSort",
    "dojo/store/JsonRest",
    "gridx/core/model/cache/Async",
    "dojo/domReady!"
  ], function(Grid, Resizer, SingleSort, JsonRest, Cache) {

It’s the same as the last time, with the exception of the new line 4, which includes the SingleSort module.

Here’s the updated Grid object. The only part that changed is including the SingleSort module in the modules array (line 8).

    grid = new Grid({
      id: "my_gridX",
      cacheClass: Cache,
      store: store,
      structure: columns,
      modules: [
      	Resizer,
      	SingleSort
      ]
    });

Once we’ve done this, we have a grid that can be sorted.

Gridx 7 - Unsorted and Sorted

dGrowl Redux – Using a Dojo Module Path Resource to Include the Library

In light of recently figuring out how to use a Dojo Module Path Resource to make a Dojo library available in an XPages application, I wanted to revisit the way I’ve been including dGrowl. In this post, I’ll show how to include it in a cleaner way.

dGrowl in XPages

dGrowl is a third party Dojo plugin that provides growl-style messages that are much nicer than Dojo Toaster.

This episode of NotesIn9 explains how to implement dGrowl in XPages and this post shows how to add two more notification styles.

Module Inclusion – Take 1

To use dGrowl (after adding the files to the application’s WebContent folder), you need to include a stylesheet and include the module in the application.

The original method I used (based on a trick found in Mastering XPages) was to add a script resource and build out the path to the main.js file, relative to the current application.

<xp:this.resources>
  <xp:script clientSide="true">
    <xp:this.contents><![CDATA[
      var pathParts = location.pathname.split('/');
      pathParts.pop();
      dojo.registerModulePath("dGrowl", pathParts.join('/') + "/dGrowl/main");			
    ]]></xp:this.contents>
  </xp:script>

  <xp:dojoModule name="dGrowl"></xp:dojoModule>
  <xp:styleSheet href="/dGrowl/dGrowl.css"></xp:styleSheet>
</xp:this.resources>

Lines 4-6 build the path to the script file and register it via dojo.registerModulePath().

Line 10 includes the module on the page.

Module Inclusion – Take 2

Now that I’ve seen how to use a Dojo Module Path Resource, this can be simplified. This resource executes the same Dojo method to register a module path.

Using this, I can dramatically simplify the page resources as follows:

<xp:this.resources>
  <xp:dojoModulePath url="/dGrowl/main" prefix="dGrowl"></xp:dojoModulePath>
  <xp:styleSheet href="/dGrowl/dGrowl.css"></xp:styleSheet>
</xp:this.resources>

The Dojo Module Path Resource just needs a URL relative to the WebContent folder of the current application. The prefix defines how to refer to the resource.

Using AMD loading, I can now initialize the dGrowl object like this:

<script type="text/javascript">
var dg;
 	
require([
  "dGrowl",
  "dojo/domReady!"
], function(dGrowl) {
 	
  dg = new dGrowl({'channels':[
    {'name':'error', 'pos':1},
    {'name':'info', 'pos':2}
  ]})
});
</script>

Line 2 declares a global variable that I can use anywhere on the page to load a dGrowl message.

Line 5 includes the module (using the name I gave it in the module resource path prefix) and line 7 makes it available to the following function under the name dGrowl.

Now, I can display dGrowl messages from anywhere on the page the same way I did in the other examples. Here’s an example that will display an informational message:

dg.addNotification('Here is an informational message',{'channel':'info', 'duration': 3000});

That’s it. This is definitely a much cleaner way to make the code available on the page.

Gridx in XPages – 6: Adding a Feature Module (Column Resizing)

In the last few posts, we’ve looked at how to modularize Gridx resources for easy reuse and generate a grid with live data from a REST service. In this post, we’ll see how to load modules for additional features in gridx, starting with the ability to resize columns.

Gridx Series

Gridx in XPages — Entire Series

Modular Design

Gridx was built from the ground up with a modular design. Core modules such as the grid header and body are always automatically loaded. Additional features are similar to Dojo EnhancedGrid plugins in that they are loaded only when specified.

This allows the grid to minimize its footprint by only loading the required resources.

To add a module, include it in the require() statement and add it to the grid object’s module attribute.Let’s take a look with a simple example…

ColumnResizer

The column definitions define a default column size, but the ColumnResizer module allows grid column widths to be re-sized by the user.

When you hover the mouse pointer over a column header border, it will change to an arrow pointing left and right and you can click and drag to re-size.

If you look on the Net/Network tab of your browser’s developer tools, you’ll see that the page now also loads ColumnResizer.js in order to enable this feature.

Example Code

Here’s a working example, building on the example in the last post:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xc="http://www.ibm.com/xsp/custom">

<xc:ccGridxResources></xc:ccGridxResources>

<div id="gridX_Here" style="height:300px; width:300px;"></div>

<script>
  require([
    "gridx/Grid",
    "gridx/modules/ColumnResizer",
    "dojo/store/JsonRest",
    "gridx/core/model/cache/Async",
    "dojo/domReady!"
  ], function(Grid, Resizer, JsonRest, Cache) {

    var store = new JsonRest({
      idProperty: '@noteid',
      target: "X_REST.xsp/gridData"
    });

    var columns = [
      {id: 'first', field: 'firstname', name: 'First', width:	'70px'},
      {id: 'last', field: 'lastname', name: 'Last'},
      {id: 'state',field: 'state', name: 'State', width: '40px'}
    ];

    grid = new Grid({
      id:	"my_gridX",
      cacheClass: Cache,
      store: store,
      structure: columns,
      modules: [
      	Resizer
      ]
    });

    //Put it into the DOM tree.
    grid.placeAt('gridX_Here');
    grid.startup();

  });
</script>
</xp:view>

Line 11 shows the module being included in the code.

Line 15 shows that I’m making the module available as Resizer.

Lines 33-35 show the modules attribute being added to the grid with the ColumnResizer module being added (via the name I gave it in line 15, Resizer).

As we add more modules to the grid, they will be added as additional entries in the modules array, so this demonstrates the general structure for adding features.

Gridx in XPages – 5: Modularizing Common Resources

The second post in this series showed how to use the djConfig property to make the Gridx (or any other library’s) resources available to an XPage. Looking forward, I wanted to find a way to modularize the common resources required by Gridx so they do not need to be included on every XPage. In this post, I’ll describe the challenges and then show a much cleaner solution that I found that makes it easy to reuse the resources.

Gridx Series

Gridx in XPages — Entire Series

Modularization Challenges

I thought it would be pretty straight forward to move the style sheets, djConfig property, and beforePageLoad code to define the modulePath to a custom control and reuse it across any pages that display a Gridx grid. Unfortunately, it wasn’t that simple.

The style sheets and the beforePageLoad code worked fine from a separate custom control, but the djConfig property only worked when on the XPage. This would still be an improvement in reusability, but would require setting that property on all XPages that display a grid.

I then thought that I could set the djConfig property in the application’s XSP properties. While that works for other djConfig attributes, it does not work for modulePaths because it still has the same escaping problem that necessitated the beforePageLoad code to begin with.

Dojo Module Path Resource

Fortunately, I found a much simpler solution – a Dojo Module Path Resource!

It’s a page-level resource, just like a style sheet, script library or Dojo module. It makes it much simpler to reference the Gridx code within the application.

  1. From an XPage or custom control, go to the Properties view and select the Resources subtab.
  2. Click the Add… button and select Dojo Module Path Resource…
  3. Enter gridx as the prefix and /gridx as the url

Gridx 5 - Dojo Module Resource

This tells the page to look for a module named gridx at the location /gridx. The forward slash starts from the WebContent folder in the application and we put the code into a folder named gridx. (Adjust as needed if your directory structure is different.)

This is much simpler; it removes the need for the djConfig property and the beforePageLoad code!

Common Gridx Resources

Unlike the djConfig property, specifying a Dojo Module Resource Path works from a custom control included on a page, so now we can separate the common resources into a custom control for easy reuse.

Now, I have a custom control named ccGridxResources. This is the full source:

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

  <xp:this.resources>
    <!-- Claro Theme -->
    <xp:styleSheet href="/.ibmxspres/dojoroot/dijit/themes/claro/claro.css"></xp:styleSheet>
    <xp:styleSheet href="/.ibmxspres/dojoroot/dijit/themes/claro/document.css"></xp:styleSheet>

    <!--  Module Path to locate Gridx Library -->
    <xp:dojoModulePath url="/gridx" prefix="gridx"></xp:dojoModulePath>

    <!-- -Main GridX Stylesheet  -->
    <xp:styleSheet href="gridx/resources/claro/Gridx.css"></xp:styleSheet>
		
  </xp:this.resources>
	
</xp:view>

Line 10 is the Dojo Module Path Resource that specifies where to find the files. The rest are the 3 stylesheets that we were already including. The djConfig property and beforePageLoad code are no longer required.

Streamlined XPage to Create the Grid

Now, this is the full source of the XPage required to create the same grid as the last post, accessing live data via a REST service.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xc="http://www.ibm.com/xsp/custom">

<xc:ccGridxResources></xc:ccGridxResources>

<div id="gridX_Here" style="height:300px; width:300px;"></div>

<script>
  require([
    "gridx/Grid", "dojo/store/JsonRest",
    "gridx/core/model/cache/Async", "dojo/domReady!"
  ], function(Grid,JsonRest, Cache) {


    var store = new	JsonRest({
      idProperty: '@noteid',
      target: "X_REST.xsp/gridData"
    });

    var columns = [
      {id: 'first', field: 'firstname', name: 'First', width:	'70px'},
      {id: 'last', field: 'lastname', name: 'Last'},
      {id: 'state',field: 'state', name: 'State', width: '40px'}
    ];

    grid = new Grid({
      id: "my_gridX",
      cacheClass: Cache,
      store: store,
      structure: columns
    });

    //Put it into the DOM tree.
    grid.placeAt('gridX_Here');
    grid.startup();

  });
</script>
</xp:view>

Now we have a more modular design and a simpler page structure to work with as we get ready to add features to the grid.

Follow

Get every new post delivered to your Inbox.

Join 56 other followers