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.