A Flexible View Control for XPages Part 6 – Sorting

When I started building the Flexible View Control, one of the driving forces was to significantly reduce the amount of back-end Domino views that would collect in my applications. I was just as guilty as anyone of creating a new view with the same selection formula and columns as an existing view with the only difference being the column arrangement or how the new view was sorted/categorized.

Build a Client-Side Sorting Demo

If you’ve used DataTables then you know that by default DataTables makes sorting your data by any column very easy. As a result, The Flexible View Control, being a mashup of Domino XPages and DataTables, has advanced sorting capabilities baked in.

Create a new XPage

To demonstrate this, I start by making a copy of my viewBasic XPage and call it viewBasicSorting.

Since I want to re-use this new XPage for multiple demos, I’m making my viewKey dynamic by having it look for a querystring parameter to get its value:

Using a url parameter to populate the viewKey

This means anytime I load this XPage I need to supply the name of the desired View Definition in the url:

viewBasicSorting.xsp?viewdef=used-cars-sorting-default

Create the View Definition

Now I create my new View Definition. Note that we are using an existing view and existing rest service to fetch the data.

View Definition with default sorting

In the screenshot above, there is no value in the Client Sort (1) field, meaning when the DataTable is built client-side it will display in the order the data was loaded from the rest service. If your back-end data is sorted by the 1st column, then that is how the data will display in the constructed DataTable.

viewBasicSorting.xsp?viewdef=used-cars-sorting-default

By default the id column is sorted

Adding a client-side sort

But what if your users insist on another view that sorts by Price with the Price column being first? In Notes/Domino, you begrudgingly copy your view, move the Price column to the front and make sure it’s sorted. UGH!

This is where the “Flexible” part of the Flexible View Control comes in. All I have to do is create a new View Definition, point it to the same data, and simply drag the Price column to the top and designate that as the sort column in the Client Sort (1) field. In my case I want to sort it descending.

View Definition sorted by price
Important: The value entered in the Client Sort must match the itemName of the column you want to sort.

The results

Now, I can load my view viewBasicSorting XPage and simply change the viewdef url parameter to show a different “virtual view”, in this case By Price:

viewBasicSorting.xsp?viewdef=used-cars-sorting-byprice

Same data from same view now sorted by price

Recap

Using one back-end view, and one XPage equipped with the Flexible View Control, we can display countless views to the front-end user by simply making a View Definition for each front-end view we need to display. Cool!

Advanced Sorting

When specifying the column to sort in a View Definition we use the Client Sort (1) field. But you may have noticed a field below that named Client Sort (2) and perhaps you wondered what purpose this serves.

DataTables has incredible advanced sorting capabilities, as the gif below illustrates.

Advanced DataTables sort capabilities

To get the multiple column sorting capability in the Flexible View Control, we use the Client Sort (2) field. To demonstrate I created a new View Definition that points to the same Domino view we’ve been using. This time, I make the Client Sort (1) by Year (descending) and add Price as a secondary sort, also descending.

When I look at the By Year view I can see that it has a primary sort (by year) and secondary sort (by price).

Sorting Server-side

My preference is to do all sorting client-side and have my back-end views act as fairly static tables of data that are indexed well and can respond quickly to requests. The View Definition does have a “Server Sort” field but it currently is not operable.

However, if you do need to sort server side before returning data view a rest service or xAgent you can certainly do so.

Important: to resort a back-end view server-side you must set the column sort setting to “both” for any column you want to be able to resort by.

To demonstrate sorting the price column I start by creating a new View Definition with pretty much all the defaults except I move the Price column to the first position (Note: the column you want to be the first sort does NOT have to be in the first position).

Next, I update the rest service that has been used repeatedly to look for the sortColumn and sortOrder query string parameters:

<xe:restService id="restService1" pathInfo="used-cars-phil-chevy-tahoe-basic"
		state="false">
		<xe:this.service>
			<xe:viewJsonService systemColumns="2"
				viewName="xspPhilPaChevTahoe" defaultColumns="true" count="1000">
				<xe:this.databaseName><![CDATA[#{javascript:@DbName()[0]+"!!demos\\used_cars.nsf"}]]></xe:this.databaseName>
				<xe:this.sortColumn><![CDATA[#{javascript:context.getUrlParameter("sortColumn")!="" ? context.getUrlParameter("sortColumn") : ""}]]></xe:this.sortColumn>
				<xe:this.sortOrder><![CDATA[#{javascript:context.getUrlParameter("sortOrder")!="" ? context.getUrlParameter("sortOrder") : ""}]]></xe:this.sortOrder>
			</xe:viewJsonService>
		</xe:this.service>
	</xe:restService>

Finally, the Flexible View Control has a queryString parameter that can be utilized to pass url parameters to the rest service being called to retrieve data.

context.getUrlParameter("viewdef") == "used-cars-sorting-server-byprice" ? "&sortColumn=PRICE&sortOrder=descending" : ""

The Results

viewBasicSorting.xsp?viewdef=used-cars-sorting-server-byprice

Price column sorted server-side

While we get the desired result, notice that there is no indication in the column header that Price is the sorted column. Only by examining the data can you determine that.


A Flexible View Control for XPages Part 4 – Callbacks & Click Events

Regardless of the type of application you’re working with, when interacting with view data you typically want to be able to take some type of action on that data, such as open a document or manipulate the data in some way.

Callbacks

DataTables, being very callback “heavy”, has two callbacks that can be utilized to add row-level functionality to take action on a clicked/double-clicked row and its data:

CallbackFrom the DataTables documentation
rowCallbackThis callback allows you to ‘post process’ each row after it have been generated for each table draw, but before it is rendered into the document.
createdRowThis callback is executed when a TR element is created (and all TD child elements have been inserted), or registered if using a DOM source, allowing manipulation of the TR element.

The Flexible View Control has properties that make incorporating these callbacks into your table build quite easy.

Flexible View Control createdRow and rowCallback

The View Control has a scriptBlock that builds a JavaScript object at runtime which contains the DataTable initialization code. This initialization code contains placeholders for rowCallback and createdRow (as well as other callbacks) that are evaluated by Expression Language to create function calls at runtime. The necessary function parameters are passed in automatically.

Callbacks created with Expression Language

You’ll notice in the createdRow callback above that by default the documentId of each row is added as a class parameter. As you will see, this makes it easy to identify rows that have been clicked/double-clicked when needing to take action.

The Flexible View Control source code has a default rowCallback function that can be utilized to add some basic click/double-click actions. Building on the demo created in the previous post, I’m going to add this rowCallback to that control.

Adding the rowCallback function to the control

Note: It’s important to use the correct syntax when supplying a function to a callback property in the view control. Since Expression Language is being used to dynamically create the function call, the “()” are not needed.

CorrectccRestView.defaultRowCallback
IncorrectccRestView.defaultRowCallback()

The defaultRowCallback code is below:

defaultRowCallback : function(row,data,index,params,o) {
		var attr = $(row).attr("data-docid");
		
		// For some browsers, `attr` is undefined; for others, `attr` is false. Check for both.
		if (typeof attr !== typeof undefined && attr !== false) {
		  // Element has this attribute
			return;
		}
		
		// add some row attr
		$(row).attr("data-docid",data['@unid']);
		$(row).attr("data-xpage",data[o.xpage]);

                // These are defined by the return values field on the view definition
		for (var x=0;x<params.length;x++) {		
			params[x] != "@unid" ? $(row).attr("data-value-"+params[x],data[params[x]]) : "";
		}
		
		var retData = ccRestView.getReturnData(o,data);
	
		$(row).attr("data-return",JSON.stringify(retData));
		$(row).click(function(ev) {
		
			// 	Get the row data 
			var retData = $(row).attr("data-return");

			if ($("td",$(this)).hasClass("rowSelectOn")) {
							
				if (o.multiValue=="true") {
					
					if (ev.ctrlKey && ev.shiftKey) {
						// do nothing
						
					} else if (ev.ctrlKey) {
						// Remove the selected class from the selected row
						ccRestView.removeSelectedRow(o,retData,this);
						
					} else if (ev.shiftKey) {
						
						if (index > window[o.thisView].config.firstIndex) {
							
							$("."+o.dataTableClass + " tbody tr").each(function(rowIndex) {
								
								if (rowIndex >= window[o.thisView].config.firstIndex && rowIndex <= index) {
									retData = $(this).attr("data-return");
									ccRestView.insertSelectedRow(o,retData,this)
								}
							})
						} else {
							console.log("select rows " + index + " to " + window[o.thisView].config.firstIndex);
						}
					} else {
						// no keys pressed.  select this row only
						
						ccRestView.clearSelectedRows(o,this);								
						ccRestView.insertSelectedRow(o,retData,this);
						window[o.thisView].config.firstIndex = index;
					}
				} else {
					// Remove the selected class from the selected row
					ccRestView.removeSelectedRow(o,retData,this);
				}
							
			} else {
				
				if (o.multiValue!="true") {
					// Remove the selected class from all rows first
					ccRestView.clearSelectedRows(o,this);
					ccRestView.insertSelectedRow(o,retData,this);
				} else {
					// multi
					if (ev.shifKey && ev.ctrlKey) {
						
					} else if (ev.ctrlKey) {
						ccRestView.insertSelectedRow(o,retData,this);
						window[o.thisView].config.firstIndex = index;
					} else if (ev.shiftKey) {
						ccRestView.clearSelectedRows(o, this);
						
						if (index > window[o.thisView].config.firstIndex) {
							
							$(o.dataTableClass + " tbody tr").each(function(rowIndex) {
								
								if ($(this).attr("data-index") >= window[o.thisView].config.firstIndex && $(this).attr("data-index") <= index) {
									retData = $(this).attr("data-return");
									ccRestView.insertSelectedRow(o,retData,this)
								}
							})
						} else {
							console.log("select rows " + index + " to " + window[o.thisView].config.firstIndex);
						}
					} else {
						ccRestView.clearSelectedRows(o,this);
						ccRestView.insertSelectedRow(o,retData,this, function() {
							
						});
						window[o.thisView].config.firstIndex = index;
					}
					
				}
						
			}
			
		}); // end click
		
		$(row).dblclick(function() {
			
			// get the unid of the double clicked row
			var docid = $(this).attr("data-docid");
			href = location.href.split(".nsf");
			location.href=href[0]+".nsf/"+o.xpage+".xsp?documentId="+docid+"&action=editDocument";
		});

Click Events

So what exactly is the rowCallback doing?

First, some attributes are added to the row dom to make it easier to reference this row by the @unid when it’s clicked or double-clicked:

var attr = $(row).attr("data-docid");
		
		// For some browsers, `attr` is undefined; for others, `attr` is false. Check for both.
		if (typeof attr !== typeof undefined && attr !== false) {
		  // Element has this attribute
			return;
		}
		
		// add some row attr
		$(row).attr("data-docid",data['@unid']);

The data-xpage attribute is added to store the XPage that should be opened when this row is double-clicked. This value typically comes from the View Definition:

$(row).attr("data-xpage",data[o.xpage]);

Next, more parameters are added. This portion of the code is useful when data besides @unid needs to be extracted from a row and passed on to another process.

// These are defined by the return values field on the view definition
		for (var x=0;x<params.length;x++) {		
			params[x] != "@unid" ? $(row).attr("data-value-"+params[x],data[params[x]]) : "";
		}
		
		var retData = ccRestView.getReturnData(o,data);
	
		$(row).attr("data-return",JSON.stringify(retData));

Now, we add the click event to every row. When a row is clicked the following evaluations are made:

  • Does this view allow multiple selections?
    • If so, check for other key presses (ctrl, shift). The control handles multiple selections like windows explorer.
  • Is this row already selected?
    • If not, add a class that changes the row color and add the row @unid to a hidden field (more on this in the next post).
    • If so, remove the class that changes the row color and remove the @unid from the hidden field.
        $(row).click(function(ev) {
         
            //  Get the row data 
            var retData = $(row).attr("data-return");
 
            if ($("td",$(this)).hasClass("rowSelectOn")) {
                             
                if (o.multiValue=="true") {
                     
                    if (ev.ctrlKey && ev.shiftKey) {
                        // do nothing
                         
                    } else if (ev.ctrlKey) {
                        // Remove the selected class from the selected row
                        ccRestView.removeSelectedRow(o,retData,this);
                         
                    } else if (ev.shiftKey) {
                         
                        if (index > window[o.thisView].config.firstIndex) {
                             
                            $("."+o.dataTableClass + " tbody tr").each(function(rowIndex) {
                                 
                                if (rowIndex >= window[o.thisView].config.firstIndex && rowIndex <= index) {
                                    retData = $(this).attr("data-return");
                                    ccRestView.insertSelectedRow(o,retData,this)
                                }
                            })
                        } else {
                            console.log("select rows " + index + " to " + window[o.thisView].config.firstIndex);
                        }
                    } else {
                        // no keys pressed.  select this row only
                         
                        ccRestView.clearSelectedRows(o,this);                               
                        ccRestView.insertSelectedRow(o,retData,this);
                        window[o.thisView].config.firstIndex = index;
                    }
                } else {
                    // Remove the selected class from the selected row
                    ccRestView.removeSelectedRow(o,retData,this);
                }
                             
            } else {
                 
                if (o.multiValue!="true") {
                    // Remove the selected class from all rows first
                    ccRestView.clearSelectedRows(o,this);
                    ccRestView.insertSelectedRow(o,retData,this);
                } else {
                    // multi
                    if (ev.shifKey && ev.ctrlKey) {
                         
                    } else if (ev.ctrlKey) {
                        ccRestView.insertSelectedRow(o,retData,this);
                        window[o.thisView].config.firstIndex = index;
                    } else if (ev.shiftKey) {
                        ccRestView.clearSelectedRows(o, this);
                         
                        if (index > window[o.thisView].config.firstIndex) {
                             
                            $(o.dataTableClass + " tbody tr").each(function(rowIndex) {
                                 
                                if ($(this).attr("data-index") >= window[o.thisView].config.firstIndex && $(this).attr("data-index") <= index) {
                                    retData = $(this).attr("data-return");
                                    ccRestView.insertSelectedRow(o,retData,this)
                                }
                            })
                        } else {
                            console.log("select rows " + index + " to " + window[o.thisView].config.firstIndex);
                        }
                    } else {
                        ccRestView.clearSelectedRows(o,this);
                        ccRestView.insertSelectedRow(o,retData,this, function() {
                             
                        });
                        window[o.thisView].config.firstIndex = index;
                    }
                     
                }
                         
            }
             
        }); // end click

The double-click event is much simpler. It simply opens the selected document using the XPage defined on the View Definition:

$(row).dblclick(function() {
			
			// get the unid of the double clicked row
			var docid = $(this).attr("data-docid");
			href = location.href.split(".nsf");
			location.href=href[0]+".nsf/"+o.xpage+".xsp?documentId="+docid+"&action=editDocument";
		});

Selecting Multiple Rows

Set the multiValue property of the view control to true:

Now, I can select multiple documents by holding <ctrl> or <shift> when clicking:

Building a Demo

In order to demonstrate everything described above, I’m going to make a copy of my viewBasic XPage that was created in Part 3, call it viewBasicCallbacks and add the rowCallback:

I’m leaving everything else intact – using the same settings and the same View Definition.

To demo the double-click, I create a basic XPage to display a used car record and I need to update my View Definition so that when I double click a row in the view it opens this carDocument XPage:

carDocument XPage
Adding carDocument to the View Definition
Double-clicking a row opens the selected document with the carDocument XPage

Here’s the link: http://demos.xpage.me/demos/datatables-xpages-bootstrap.nsf/viewBasicCallbacks.xsp

In the next post …

We will see how the selected rows can be accessed both client and server side.


A Flexible View Control for XPages Part 3 – Create a Basic View

See the demo live

You can see the demo created in this post here.  As more demos are added to demonstrate the many features of the Flexible View Control, they too will be available online.

In Parts 1 & 2 we learned about the Fexible View Control and created the configuration we needed to get started with our application.

The Data

Now, we are going to create a view from scratch utilizing a database of 2 million records comprised of used car data.

To get started I need to add the used car database to my configuration document so the View Definition XPage can read the views in this database.

As mentioned in a previous post, and as you can see above, I’m using Bootstrap (3.4) in my application for my UI framework.  I’ve already created a navigator custom control:

and an XPage template I’m going to use to create all of my demo pages:

<?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" styleClass=""
	style="overflow:hidden">
	<xp:this.resources>
		<xp:script src="/ssjsCCRestView.jss" clientSide="false"></xp:script>
	</xp:this.resources>
	<div class="level0-flex-container">
		<div class="level0-flex-item">
			<xc:ccNav></xc:ccNav>
		</div>
		<xp:panel styleClass="actionBar" id="panelActionBar" style="">


		</xp:panel>
<!-- Drag ccRestView here


 -->

	</div>
</xp:view>

We’re going to get started with the most basic implementation of the Flexible View Control. In my used car database, I have a view xspPhilPaChevTahoe which shows all of the Chevy Tahoes in Philadelphia, PA. Normally I wouldn’t create a single purpose view like this – it’s being done for demonstration purposes only. In a database of 2M records, this view contains 70 and I’m going to display it with the Flexible View Control.

The View Definition

First, I create a View Definition and point it to the xspPhilPaChevTahoe view in the used car database and select all of the columns.

The REST Service

Next, I create the Rest Service that will be used to fetch the data. We told the View Definition to find the REST service on the restServices XPage. This is where I add mine and give it a path-info that matches the key I used in my View Definition above.

After building the application I do a quick test to make sure I’m getting data from the Rest Service:

The XPage

To create my page where I want to put my view, I create a copy of the XPage template and call it viewBasic. I also add a link to the Open button in my navigator custom control that will open this new XPage.

The Flexible View Control

Now, I’m ready to add the control to my new XPage by dragging it from the custom control palette:

viewBasic XPage before custom control is added
viewBasic XPage after custom control is added (source view)
viewBasic XPage after custom control is added (design view)

After adding the custom control, take a look at the custom properties by clicking on the control and then clicking the Custom Properties on the Properties tab. There are a lot of properties (some of which have default values) which are integral to making this the “Flexible” View Control for XPages. Some of these properties get passed on to DataTables as the view is constructed.

ccRestView custom properties

Most importantly, there are three properties that must be set in order for the control to work properly.

PropertyDescription
thisViewUnique identifier for this view. This value is used in the internals of the custom control to get a handle on this instance of the view control.
viewKeyThis value refers to the View Definition that the control will use to get its configuration and location of rest data.
dataTableClassA CSS class name that gets applied to the DataTable and is used to refer to the table programmatically. This value should be unique.

On my viewBasic XPage I use the following values:

The Results

After saving and building I load my viewBasic XPage in the browser and verify I am getting the results I expect:

What we have now in our browser is a DataTable (v. 1.10.19) and all of the standard front end tools that come baked into that framework, such as filtering, sorting, etc.

Recap

The purpose of this post was to demonstrate how easy it is to quickly add a view to your application using the Flexible View Control for XPages:

  1. Create a View Definition.
  2. Create a REST Service (or reuse an existing service) that points to your Domino view.
  3. Add the custom control to your XPage and point it to your View Definition

Next

In the next post, I’m going to take this simple example and start adding advanced functionality to create more functional views.