Reader question – How does the Flexible View Control handle response documents?

Recently, I received a comment on a blog post asking how the FVC handles response documents:

One question I have is how best to deal with response documents in views, I am not sure if I have missed something in the tutorials but I am unsure how to render responses or if the control handles views with responses?

In building the control, response documents were never even a consideration for me since we don’t use them in our environment so the Flexible View Control does not contain specific functionality for handling response documents. However, because the framework was built to be as flexible and configurable as possible, the tools provided make creating that functionality incredibly easy. Based on the comment above I created a demo to prove that:

http://demos.xpage.me/demos/datatables-xpages-bootstrap.nsf/viewGeneral.xsp?viewdef=response-docs

Building the Demo

For this demo I created a new used car database and added some records from the original used car database. In addition to the listing form that already existed for the main docs, I added a response form and response to response form so I could add comments to listings and comments to comments:

response form

On both the response and response to response forms the ID field from the original document is being inherited to have a common value to link all of the documents besides docids, but it’s not really necessary to make the demo function properly.

response to response form

Notes View Configuration

To get the View set up I need to make sure that Show response documents in a hierarchy is turned on and add the comments and parent_docid columns.

Note: No column is set to “show responses only”.

After adding a couple of responses in the Notes Client the back-end view looks like this:

The View Definition

In the View Definition I’m setting the columns for the responses to hidden because I don’t need that data to display. We *do* need the comments column to display but you will see momentarily how that will be done.

The REST Service

Another important step is making sure the REST service being utilized to serve the data contains the appropriate document hierarchy information. Since we are using the standard <xe:viewJsonService>, the system columns can be configured to include the position data in addition to the unid. By default, all of the columns are enabled but I prefer to remove the ones I don’t need to cut down on payload size.

Now we have the data we need:

Configuring the Flexible View Control

So after all this setup we still haven’t actually displayed anything with the control yet. How do we go about doing that? Before adding functionality to create the response hierarchy in the control the response docs look like standard docs, but with little information:

The Callbacks

The first step I take to build the response hierarchy is adding a createdRow callback function to the control. Normally, I would probably use a rowCallback out of habit but in the process of tinkering around, I moved my rowCallback code into the createdRow callback. Here is what the DataTables documentation says about each:

rowCallback
This 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. This means that the contents of the row might not have dimensions ($().width() for example) if it is not already in the document.

This function might be used for setting the row class name or otherwise manipulating the row’s tr element (although note that createdRow can often be more efficient).

createdRow
This 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.

This is particularly useful when using deferred rendering (deferRender) or server-side processing (serverSide) so you can add events, class name information or otherwise format the row when it is created.

There are a few things that need to be accomplished in the createdRow:

  • For responses, reconstruct the response row to be more like the “classic” Notes response document and add classes and attributes that connect it to its parent row.
  • For non-responses, add that document’s docid as a row class and add a click event that will expand and collapse any responses.
  • For responses with responses, add a click event to expand/collapse its child documents.
  • Use the position data to provide classes and attributes to the response rows to make it easier to build our hierarchy.
rowCallbackResponse : 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;
	}
	
	// action item view on pcspDocument
	$(row).attr("data-docid",data['@unid']);
	
	// get the colspan
	var colspan = $('td',row).length;
	
	// if this is a response then add some info to this row
	var position = data['@position'];
	
	if (position.split('.').length > 1) {
		// remove cells and replace with 1
		$(row).html('<td colspan="' + colspan + '"><div><i class="fa fa-comment-o right5"></i>'+ data['comments'] + '</div></td>')
	
		// add some classes and attributes
		$(row).addClass('response response-'+data['parent_docid']);
		$(row).attr('data-position',position);
		$(row).addClass('response-level-'+position.split('.').length)
		
		// add a reference to the parent doc
		$(row).attr('data-parent-docid',data['parent_docid']);
		
	} 
	
	$(row).click(function(ev) {
		$('[data-position^="' + position + '."]').toggleClass('hidden');
		$('td:first div .fa',this).toggleClass('fa-caret-down');
		$('td:first div .fa',this).toggleClass('fa-caret-right');
	});	
}

I also add a little CSS to create the indentation of the responses:

table.dataTable tr.response-level-2 td {padding-left:100px}
table.dataTable tr.response-level-3 td {padding-left:125px}
table.dataTable tr.response-level-4 td {padding-left:150px}

Now looking at the View, we are starting to see a result more like what we would expect. We can expand and collapse the responses by clicking the parent row and any response row that has responses, but we have a little bit more work to do. I need to add a visual indicator to those rows so the user can see they have responses.

To do that I’m going to add a drawCallback function. With that I want to accomplish the following:

  • Add a visual indicator to any row that has response documents. Using FontAwesome, a twistie icon is added to the first column of any row with responses to simulate the classic Notes look. The great thing is this is completely customizable. I chose the first column for simplicity.
  • Move the node of each response so they appear after the parent. This is not an issue when the View first loads, but is necessary when the View is sorted or filtered.
drawCallbackResponse : function() {
		
	// retrieve data from browser sessionStorage
	var responses = sessionStorage.getItem('responses') != '' ? JSON.parse(sessionStorage.getItem('responses')) : {};

	// loop through each item 
	// responses are stored in an object with the parent docid as key
	for (var key in responses) {
		if (responses.hasOwnProperty(key)) {
			// prevent dupes 
			$('[data-parent-docid="' + key + '"]').remove();
			var pos = '.'+key;
			// insert each response after
			for (var x=0;x<responses[key].length;x++) {
				pos = $(responses[key][x]).insertAfter(pos);
			}
		}
	}
	
	$('.response').each(function() {
		var post = $('.'+$(this).attr('data-parent-docid'));
	
		if ($('.'+$(this).attr('data-parent-docid') + ' td:first div .fa-caret-down').length==0 &&
			$('.'+$(this).attr('data-parent-docid') + ' td:first div .fa-caret-right').length==0
		) {
			$('.'+$(this).attr('data-parent-docid') + ' td:first div').prepend('<i class="fa fa-caret-down right5"></i>');
		}
		pos = $(this).insertAfter(pos);
		
		if ($('.fa').hasClass('fa-caret-right')) {
			$(this).addClass('hidden');
		}
	});
}

Now we have a fully functional View with response documents in a hierarchy. We can click the column headers to change the sort order of the View and the responses will still travel with their parent.

But we still have an issue .. filtering the View will make the responses disappear because the response rows don’t contain the same data as their parent.

Again, we have all of the tools we need to overcome this. Using the initComplete callback functionality of the control, we can store the html of all response rows in the sessionStorage of the browser when the View initialization is complete:

initCompleteResponse : function(_v,dataTableClass) {
	var responses = {};
	var oa = [];
	var o = {};
	
	$('.response').each(function() {
		//store the html of response rows 
		if (typeof (responses[$(this).attr('data-parent-docid')]) == 'undefined') {

			responses[$(this).attr('data-parent-docid')] = [];
		} 
		var html = $(this).clone().wrap('<p/>').parent().html();
		responses[$(this).attr('data-parent-docid')].push(html);
	});
	sessionStorage.setItem('responses',JSON.stringify(responses));
}

You may have noticed in the drawCallback function there was a reference to the sessionStorage:

// retrieve data from browser sessionStorage
var responses = sessionStorage.getItem('responses') != '' ? JSON.parse(sessionStorage.getItem('responses')) : {};

By doing this little bit of caching we can ensure that we can get the responses back into the table even if the DataTables filtering removes these records from the display.

Conclusion

The combination of the createdRow (or rowCallback), initComplete, and the drawCallback functions give us the power to rebuild the response hierarchy of a Notes View in the Flexible View Control for XPages.


Humbled to be an HCL Ambassador for 2021

HCL has announced the HCL Ambassador Class of 2021 and I’m very honored to be on this list for the first time.

The HCL Ambassadors Program recognizes customers and partners who go above and beyond to promote HCL Software products, engage with the community, and help others.

Thank you to those who nominated me, and to HCL for selecting me based on the criteria above. We’ve all had enough of 2020 at this point, so here’s to 2021!


Flexible View Control for XPages – Categorized Views

HCL Digital Week made very clear the emphasis in the application development space is on “low-code”. That got me thinking about the Flexible View Control and how, at the end of the day, it really is a low-code tool to add View data to XPage applications. Just like with Domino Volt, many aspects of configuring a View with the Flexible View Control are as simple as drag, drop, and configure.

Creating a categorized View is no different. Let’s take a look at just how easy it is by creating a View of all the Ferraris listed in the used car database. Naturally, we start by creating the View Definition:

The View Definition

The View Definition being used for the demos shown in this post can be found here.

I want to categorize this View by the MODEL column. Just like we do for a Notes View, I make that the first column. Next, check off the “category” checkbox in the last column of the MODEL column row.

A couple of important notes in the above sceenshot:

  1. The column to be categorized MUST be sorted.
  2. In the image above the MODEL column is being hidden. That might seem counter-intuitive since this is the column we are categorizing, but it will make sense in a moment.

The View XPage

Link to the demo used in this post.

BEFORE adding categorization

The image above shows what the Ferraris View looks like before adding the categorization.

The image below shows what the View looks like after adding the categorization and hiding the MODEL column.

AFTER adding categorization and hiding the MODEL column

How does it work?

To create the categorization the Flexible View Control traverses the DataTable during the drawCallback phase and when a row contains a different MODEL value, a new tr node is inserted before that row to represent the category. The reason why we can hide the MODEL column is the traversal is done on the DataTable dataset, not the actual HTML of the table. After inserting the tr node a click event is applied to provide expand/collapse functionality for the category.

Let’s break down the tr node that gets added for each category:

category tr element insert for each category
  • The data-category attribute is the unique identifier for this category. This value also gets applied to all of the child rows for this category.
  • “group” classes are added to aid in referencing the category rows.
  • The data-hide attribute is used to handle expansion/collapse of the category.
  • The table cell created inside the tr node is automatically given a colspan equal to the number of cells in a non-category row.

Doing the Twist(ie)

You probably noticed in the image above that there is a “twistie” node in the tr HTML. By default this is simply a “-” to denote expansion and a “+” to denote a collapsed category. This is handled with some simple CSS:

.twistie {margin-right:5px}
.twistie-expand::before{
	content:'-';
	font-size:1.25em;
}
.twistie-collapse::before {
	content:'+';
	font-size:1.25em;
}

Adding a little more “flair” to the twistie is easy by simply overriding the CSS. For example, I’m a big fan of Font Awesome and use that for my twistie icons:

Using Font Awesome icons for twisities
.twistie-expand::before{
	font-family: FontAwesome;
	content: "\f056";
	font-size: inherit;
}
.twistie-collapse::before {
	font-family: FontAwesome;
	content: "\f055";
	font-size: inherit;
}

Expand/Collapse All

Just as in a Notes View that contains categorization, the Flexible View Control provides the ability to expand or collapse the entire View. In our demo there are two buttons at the top that are wired with this functionality:

When the control is rendered functions are created dynamically that call functions contained in the csjsCCRestView.js script library:

collapseAll : function() {
	ccRestView.groups.collapseAll(this.thisView);
},
expandAll : function() {
	ccRestView.groups.expandAll(this.thisView);
}

The code behind the buttons can then refer directly to a specific View, which we’ve named ViewBasic in the thisView property of the control for this demo:

ViewBasic.collapseAll();
ViewBasic.expandAll();

Filtering

One of the great features of DataTables is being able to enter a query in the search box and having the contents of the DataTable reduce to the records that match that query. This functionality still works when the View is categorized!

The next post will expand on the categorization functionality by introducing subcategories and a feature called “category renderers”.