Join me at CollabSphere 2021!

CollabSphere, perennially a great conference, will be held virtually for the 2nd year in a row from Oct. 19th – Oct. 21st and I am fortunate to be presenting a session this year.

DEV115: Using XPages and Bootstrap tabs to build a (Monolithic) Single-Page Application (of Micro Front-ends)
Time: Tuesday 10/19 @ 2PM EST

While the title is a bit wordy, it is meant to be a little cheeky … This session will demonstrate how at Mutual Boiler Re we use Bootstrap tabs and jQuery to provide our XPages applications with a structure that allows multiple pages, views, and documents to be opened within one browser tab, essentially creating a Single Page Application that mimics the Notes Client in a browser.

Thank you to Richard and the Collabsphere team for this opportunity. I look forward to seeing you there!


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.


Flexible View Control for XPages – Mashing Up Total Columns and Category Renderers

In a recent blog post we covered the concept of ‘Category Renderers’, which allows the user to easily alter the visual representation of category rows in a categorized View. Combining this functionality with totals columns, highlighted in the last blog post in this series, we can further enhance Views in XPages and make them more user-friendly than their Notes counterparts.

The Mission

Continuing with our dataset from previous posts of used cars available in Wilmington, DE I want to display the average PRICE of each MAKE of vehicle. In my View Definition I categorize the MAKE column:

Note: As you can see in the screenshot below, the View Definition has an option to display average values. This option currently doesn’t function properly and will be updated in the next release.

Then I add a Category Renderer to the MAKE column:

And I still have totals enabled for PRICE and MILEAGE:

The Category Renderer

The demoFn.category.convert_total_to_avg function is a function in a client-side js library. Remember, you do not provide any parameters to the function. This function is applied to every category row and the dom object for that category is automatically passed into the function as a parameter.

convert_total_to_avg : function(_this) {
	// _this is the category row dom element
	// take a total value for a category and instead display the avg value
			
	// get the total # of rows for this category
	var total_rows = $('.total_rows',$(_this)).html();

	// get the total PRICE for this category
	var total_value = $(_this).attr('total_price_values');

	// format the column that contains the total #
	$('.MILEAGE_total',$(_this)).html(total_rows+' (total)');

	// calculate and format the average price
	$('.PRICE_total',$(_this)).html('$'+$U.formatMoney((total_value/total_rows),0)+' (avg)');
}

Notice in the code above we are extracting the total_value from the attributes of the category tr. We are able to do so because when totals are enabled for a categorized column the Flexible View Control stores the accumulated data in the tr element for easy access:

The Result

See the demo: http://demos.xpage.me/demos/datatables-xpages-bootstrap.nsf/viewGeneral.xsp?viewdef=wilmington-de-category-renderer-totals&key=city:Wilmington:state:DE

Here is our result:

What About The Footer?

While we are achieving the desired outcome for each category, the footer still displays the total PRICE for all used cars in Wilmington, DE.

How do we get the average price for all used cars instead? In the next post we’ll show you how!


A Flexible View Control for XPages Part 8 – Putting the 'Flex' in Flexible

Thus far, all of my demonstrations of the Flexible View Control have been centered around starting from scratch and integrating the view control into a mostly blank page, aside from a basic Bootstrap layout. Even the basic Bootstrap layout I’ve been using contains some very important elements that are integral to the control sizing properly, whether you are starting with a blank page or trying to integrate into an existing application.

Enter Flexbox

For years I read about Flexbox and how it can simplify the layout of pages and I always managed to push learning it to the back burner. Once I did all I could think is WHY DIDN’T I LEARN THIS SOONER?!?!? Prior to that, I probably did an embarrassing amount of dynamic element sizing with JavaScript. I can’t stress this enough … Flexbox has fundamentally changed how I approach application layout.

So what exactly is Flexbox?

The Flexbox Layout (Flexible Box) module (a W3C Candidate Recommendation as of October 2017) aims at providing a more efficient way to lay out, align and distribute space among items in a container, even when their size is unknown and/or dynamic (thus the word “flex”).

from css-tricks.com

An important concept to understand is flexible containers are not an “all-in” or “all-out” proposition. You can mix flexed elements with non-flexed elements on the same page, depending on what you’re trying to accomplish.

DataTables and Fixed Headers

Before showing how the FVC integrates Flexbox, a little background. When displaying view data in an application, if the amount of data in the view requires the user to scroll, having fixed headers naturally creates a better user experience. This can be achieved in out-of-the-box DataTables in two ways:

  • By adding the parameter ‘scrollY’ to the configuration options with a px value > 0. For example, “scrollY”: “200px”
  • By utilizing the DataTables Fixed Header plugin

The pros and cons of each based on my experience are below.

MethodProsCons
scrollYBuilt into DTRequires a hard-coded
height value.
Fixed Header pluginAlso can create a
fixed footer
Requires loading an
additional 30k js file, does not handle responsiveness well

Before using Flexbox for dynamic sizing, the Flexible View Control used the scrollY method to define an initial height and then after the table was rendered, the height of the scroll body would be recomputed so that the table would fit into the parent container. This involved a lot of taking measurements of various elements such as the table offset, header height, filter height, info height, etc. and creating a formula to calculate the correct height needed for the scroll body. It worked great but I really wanted a “cleaner” way to layout the table elements… and I found that with Flexbox.

The Flexible View Control and Fixed Header/Footer

The FVC has a parameter showFixedHeader which is set to true by default. When fixed headers are enabled, the control creates the scrollY option parameter for DataTables at runtime. The value is irrelevant .. the purpose is for DataTables to trigger the dom changes needed to make the table body scrollable:

The key elements here are dataTables_scrollHead, dataTables_scrollBody, and ffDefault. The first two are created by DataTables at initialization. The third, ffDefault is created by the FVC.

Now that we know what the elements are, we need to get them to “flex” by overriding their default styling. The snippet below displays the CSS from the FVC that allows us to accomplish that.

/********************************
 DataTables overrides 
*********************************/
.dataTables_wrapper {
	flex:1;
	display:flex;
	flex-direction:column;
}
.dataTables_scroll {
	flex:1;
	display:flex;
	flex-direction:column;	
}
.dataTables_scrollBody {
	flex:1;
	flex-basis:auto;	
	flex-basis:0;
}
.dataTables_scrollHead {
	flex:1;
	flex-basis: auto;
	flex-grow: 0;
	position:relative;
}
.dataTables_filter input {
    display: inline;
    width: 250px;
    margin-left: 5px;
}
div.dts {
	display: flex !important;
}
/************************/
FIXED FOOTER
/************************/
.ffDefault {
	flex:1;
	flex-grow:0;
	flex-basis:auto;
	height:35px; /* Not required */
	position:relative;
}

With the CSS above we have the DataTables dom elements configured to flex. But we need to do some more flexin’ to get the layout to flow the way we want. Continuing to work our way up the dom hierarchy, we flex the wrapper that the FVC places around the DataTables dom:

.panelRestViewContainer {
	display:flex;
	flex-direction:column;
	height:auto;
	position:relative;
	flex:1;
}

Remember the ultimate goal

We want our view to fit in the space being provided by the flexed layout without having to hardcode dimensions or compute dimensions dynamically. This requires the top-most flexed container defining its height either through pixels or through the viewport height (vh). The demos being used throughout this series (such as this) use the following dom hierarchy (simplified for illustration):

<body>
	<div class="level0-flex-container">
		<div class="level0-flex-item">
			Navbar
		</div>
		<!-- Flexible View Control -->
		<div class="panelRestViewContainer">

		</div>
	</div>
</body>
body, form {
	/* very important! */
	overflow:hidden;
}
.level0-flex-container {
	display:flex;
	flex-direction:column;	
	height:100vh; /* will fill the window viewport */
}
.level0-flex-item {
	flex:1;
	flex-basis: auto;
	flex-grow: 0; /* this element will size to its content */
	position:relative;
}

What this all means:

  • Body and form elements will not scroll
  • Which allows for the flex container to fill the viewport
  • The navbar, with flex-grow: 0, will only get as tall as its content
  • The Flexible View Control, with flex-grow: 1 (through the shorthand flex:1 property) will expand to fill the remaining empty space in the container.

The end result is our view filling the screen with scrollable content, just as we wanted.

More Layout Demos

The demos below illustrate how easy it is to create several different flexible layouts and to add the FVC to XPage layout controls:

  1. Starting with a flexible container
  2. Adding a header
  3. Adding the Flexible View Control
  4. Add an action bar
  5. Add a footer
  6. Add a left-side navigator
  7. Add a footer
  8. Add a right sidebar
  9. FVC with NO fixed header in a flexible layout
  10. Adding the Flexible View Control to an Editable Area
  11. Adding the Flexible View Control to an Application Layout control

A Flexible View Control for XPages Part 7 – Modals & Picklists

While I intended to get to this blog post eventually, the urgency of doing so was increased due to the following post on StackOverflow:

https://stackoverflow.com/questions/61061571/xpages-flexible-view-control

The OP also reached out to me directly looking for an answer so why not crank out a blog post with the answer?

By combining the Flexible View Control with a Bootstrap modal, you can easily mimic the Lotus Notes Picklist functionality. Getting the control working in a Bootstrap modal requires only a few simple steps, illustrated in this working demo.

Step 1 – Add the modal source to your page

I start with something like this:

<!-- Modal -->
		
		<div class="modal fade picklist" id="myModal" tabindex="-1" role="dialog"
		aria-labelledby="myModalLabel">
		<div class="modal-dialog" role="document">
			<div class="modal-content">
				<div class="modal-header">
					<button type="button" class="close" data-dismiss="modal"
						aria-label="Close">
						<span aria-hidden="true">x</span>
					</button>
					<h4 class="modal-title" id="myModalLabel">Choose a record</h4>
				</div>
				<div class="modal-body" style="display:flex;">
					<!-- place the Flexible View Control here -->
				</div>
				<div class="modal-footer">
					<button type="button" class="btn btn-default" data-dismiss="modal">
						Cancel</button>
					<button type="button" class="btn btn-primary">OK</button>
				</div>
			</div>
		</div>
	</div>

For the most part, the code above is straight off the Bootstrap site with a few important additions:

  • I added the class ‘picklist’ to the ‘modal’ div.
  • I add an inline style to ‘modal-body’ of ‘display:flex;’. This is very important since the control uses Flexbox (more on that in a future post).

Step 2 – Add the control to your page

Drop the control in the space bookmarked in the source above and configure the control as you do for a normal view page. Make sure you keep the modal-body intact!

Step 3 – Check your View Definition

It is highly recommended to have the Load on Init property unchecked. You will see why in a moment.

Step 4 – Resize the modal

By default, the Bootstrap modal is pretty small. Of course, the dimensions can be changed with css, but how do you deal with different screen sizes? By adding an event handler to the ‘show’ event of the Bootstrap modal, you can resize the modal to a percentage of available screen space:

<xp:scriptBlock>

			<xp:this.value><![CDATA[$(".picklist").on('show.bs.modal', function () {

	var mbHeight = $(window).height() * 0.8;
	$('.modal-content').css('overflow-y', 'auto'); 
	$('.modal-body').css('height', mbHeight-50);
	
	$('.modal-body').css('overflow', "auto");
	$('.modal-dialog').css('width', $(window).width() * 0.9);
	
});

</xp:scriptBlock>

In the code above, my modal-body will be 80% of the window height and 90% of the window width.

Step 5 – Load the view after the modal is opened

This is an important step due to the way DataTables draws the table and dynamically sizes the table and its cells. Initializing the table too soon results in a table that is not formatted properly:

Badly formatted DataTable when view is built before the modal has been completely rendered.

In this example above, the view is built during the ‘show’ event, resulting in the table header dimensions not matching up to the actual data rows.

Lucky for us, Bootstrap has another event that we can tap into. You may have noticed it in the screenshot above. The ‘shown’ event fires AFTER the modal has been rendered to the user:

By adding 3 lines of code to the scriptBlock above and tapping into the ‘shown’ event, our table will be built with the proper layout dimensions.

$(".picklist").on('shown.bs.modal', function () {
	viewBasic.build();
});

In the snippet above, viewBasic refers to the ‘thisView’ property of the control. Since the ‘Load on Init’ property is unchecked on the View Definition, the view needs to be triggered by its ‘build’ function.

Step 6 – Patch the csjsCCRestView.js file

After completing the steps above, the modal will work fine and the table will load and be fully functional. However, in creating this demo I realized there is an adjustment that needs to be made to the source code. This is due to legacy code that was used when many aspects of the control were being sized dynamically with javascript prior to implementing Flexbox.

The issue is the footer is being calculated based on the window’s dimensions and not the modal’s, resulting in the width of the interior of the modal being wider than the modal container. This will be updated on the github repo, but in the short term you can fix it in your database by updating the buildFooter function in the ccRestView.js file with the code below. The fixes are labeled ‘//MODAL FIX’. A few lines are commented out and a few are altered.

buildFooter : function(o) {
		
		if (o==null) { return; }
		if (o==undefined) { return; }
		console.log("=== start build footer for " + o.thisView + " ===")
		if ($(".ffDefault",$(".panel"+o.thisView)).length > 0) {
			// check to see if the info is in the footer.
			// if so, set it aside and pick it up later
			//$(".ffDefault",$(".panel"+o.thisView)).remove();
		// MODAL FIX	$(".ffDefault",$(".panel"+o.thisView)).css("max-width",$(".dataTables_scrollBody table",$(".panel"+o.thisView)).outerWidth()+"px")
		// MODAL FIX	$(".ffDefault",$(".panel"+o.thisView)).css("min-width",$(".dataTables_scrollBody table",$(".panel"+o.thisView)).outerWidth()+"px")
			 return;
		}
		var cellCount = $("th",$(".panel"+o.thisView + " " + o.dataTableClass + " thead")[0]);
		footerAttachTo = o.viewport == "" ? "body" : o.viewport;
		var tableWidth = "100%"; //$(".panel"+o.thisView + " tbody").outerWidth();
		var tdCells = [];
		var rowOne = $(".panel"+o.thisView + " tbody tr");
		//console.log("rowOne length=" + $(rowOne).html())
		// create footer cells to match header
		for (x=0;x<cellCount.length;x++) {	

			cell = $("td",rowOne).length == 1 ? $("td",rowOne) : $("td",rowOne)[x]

			var paddingLR = ($(cell).outerWidth() - $(cell).width())/2 + "px";  // css("width") //$("td",rowOne).css("padding");   // ($(o.cellCount[x]).outerWidth() - $(o.cellCount[x]).width())/2 + "px";
			var border = ($(cell).outerWidth())
			tdCells.push("<td data-column='" + o.itemIndexes[x] + "' class='" + o.itemNames[x] + "Foot' style='width:" + $(cell).width() + "px;padding:" + paddingLR + "'> </td>");  //padding-left:" + paddingLR + ";padding-right:" + paddingLR + "
		}


		var pos = $(footerAttachTo).position();
		// get the width of the fixed header so we can apply it to
		// the footer
		width = $(footerAttachTo).css("width");
		footerAtachTo = footerAttachTo != "" ? footerAttachTo : "body";
		//console.log("attaching footer to " + footerAttachTo);
		var footerHtml = "<div class='ffDefault " + o.ffClass + "' style='width:" + width + "'><table style='width:" + tableWidth + "px' class='fixedFooterTable cell-border'><tfoot><tr>" + tdCells.join("") + "</tr></tfoot></table></div>";
		if (o.showFixedHeader) {
			width="auto"; // MODAL FIX
			var newFooter = $(".dataTables_scroll",$(".panel"+o.thisView)).after("<div class='ffDefault " + o.ffClass + "' style='width:" + width + "'><table style='width:" + tableWidth + "' class='fixedFooterTable cell-border'><tfoot><tr>" + tdCells.join("") + "</tr></tfoot></table></div>");
			$(".ffDefault",$(".panel"+o.thisView)).prepend($(".dataTables_info",$(".panel"+o.thisView)))
			$(".dataTables_info",$(".panel"+o.thisView)).css({"position":"absolute","padding":"5px","float":"none"})
		} else {
			width="auto"; // MODAL FIX
			var newFooter = $(".panelRestView",$(".panel"+o.thisView)).append("<div class='ffDefault " + o.ffClass + "' style='width:" + width + "'><table style='width:" + tableWidth + "px' class='fixedFooterTable cell-border'><tfoot><tr>" + tdCells.join("") + "</tr></tfoot></table></div>");
		}
		
		if (footerAttachTo != "body") {
		  if (pos) {
			left = $(footerAttachTo).css("position") == "relative" ? 0 : pos.left;
		  }
		}
	// MODAL FIX	$(".ffDefault",$(".panel"+o.thisView)).css("max-width",$(".dataTables_scrollBody table",$(".panel"+o.thisView)).outerWidth()+"px");
	// MODAL FIX	$(".ffDefault",$(".panel"+o.thisView)).css("min-width",$(".dataTables_scrollBody table",$(".panel"+o.thisView)).outerWidth()+"px");
		console.log("=== end build footer for " + o.thisView + " ===")
	},

In conclusion

Integrating the Flexible View Control with a Bootstrap modal is easy and provides all of the functionality of DataTables you’d expect – filtering, sorting, etc.

Postscript

For applications where multiple Picklists are needed with different data, you can create a modal custom control with custom properties that can be fed into the view control dynamically. I have one application that has at least 20-25 different Picklists and this method works great.


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 5 – Processing Selected Rows

The previous post in this series demonstrated how to add click events to a view created with the Flexible View Control utilizing the callbacks that are built into DataTables. But once a row (or rows) is selected, how do you actually DO something with the selection?

Getting a Handle On Selected Rows

There is a hidden field on the control that stores the @unid of the selected rows along with any other data the View Definition configured to return when selected (in JSON format). When the control is rendered, a class is applied to the field based on the “thisView” parameter given to the control.

Client-Side Data

In the demo we built in Part 4, we gave our thisView parameter the value “viewBasic”. This allows us to reference the selected row(s) client-side in jQuery with the syntax:

$('.fldviewBasic').val();

Using the demo from http://demos.xpage.me/demos/datatables-xpages-bootstrap.nsf/viewBasicCallbacks.xsp, when I select a row and examine the hidden field in dev tools I see:

Hidden field with selected row data stored as JSON

What if I select multiple rows?

What if I want to return data besides just the row’s document id? To do so, I update the View Definition to tell it to return the columns I want when I click a row:

In this example, I want to return the ID and VIN columns in addition to @unid.

Important Note: By default, the @unid value is returned when a row is clicked. The Return Value of the View Definition overrides this value. Therefore, if values are entered in this field on the View Definition, @unid needs to be included if that value needs to be accessed.

Server-Side Data

The Flexible View Control also makes it easy to pass the selected rows server-side. The aforementioned hidden field is bound to a viewScope mapped to the thisView value.

thisView composite data bound to a viewScope

To demonstrate, I’m adding a button to my example above that does a partial refresh on a panel and executes some server-side code to examine the selected rows:

Server-side action with no document selected
Server-side action with a document selected

Recap

The Flexible View Control for XPages makes it very easy to get a handle on the rows selected in a view and process that data both client-side and server-side

In the next post …

I’ll start to demonstrate the “flexible” part of the Flexible View Control by showing how a Domino view with over 2 million records can be mined to create different representations of data with the control through the power of the View Definitions.


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.


Boot your alerts in the … with bootAlert

Unfortunately, due to the sudden illness and ultimate passing of a family member in the fall, it has been quite a while since I last blogged.  Hopefully, this post finds me getting back on the blogging horse to contribute some content to the Xpages/Domino community and bring some ideas I had been kicking around to fruition.

Today I am releasing bootAlert, a simple XPages custom control that allows developers to add configurable, reusable Bootstrap alerts to their apps without having to add any additional plugins.  You should already be using Bootstrap/jQuery in your application in order to use this custom control.

For the past few months, in working on our application migration project, I built a configurable Bootstrap alert custom control.  I found myself continuing to add features as different needs arose.  So, I thought I would release it to the community.

Why bootAlert?

  • bootAlert can be triggered from both server and client-side Javascript
  • bootAlert can use Font Awesome icons
  • bootAlert can be turned into a Growl-like message on-the-fly
  • bootAlert is dynamically configurable – one action may require the 'success' class and another may require a 'warning' or 'danger' notification.  One control can be used to display all three.
  • bootAlert can be customized with css
  • Add as many bootAlert controls to your page as you want

bootAlert with view.postScript

bootAlert can be triggered from server-side js with view.postScript()

bootAlert let's you add Bootstrap Growl messages

bootAlert let’s you add Bootstrap Growl messages to your application

Demo

I plan on submitting this as an OpenNtf project, but for now you can find a demo, as well as download bootAlert here

A github repo can be found here.

Getting Started

Getting started with bootAlert is easy.  Simply:

  • Download the demo database
  • Copy the custom control and script library into your application (or copy the contents of the script library into your existsing client-side script library)
  • Drag the custom control onto your xpage and populate the alertName property
<xc:ccBootAlert alertName="alertDemo2" id="ccBootAlertDemo2"></xc:ccBootAlert>
  • Call bootAlert from client-side js …
// Client side js 
var o = {}
o.title = "Client Side";
o.body = "This alert is being generated by client side javascript";
o.alertType = "danger";
o.alertIcon = "fa-calendar fa-lg"
bootAlert.show('alertDemo2',JSON.stringify(o))
  • or call bootAlert from server-side js by putting a value into a requestScope variable and making sure the bootAlert control is part of the partial refresh target:
// Server side js 
// This method assumes the alert is part of a partial refresh target
var o = {};
o.title = "Server Side";
o.body = "This alert is being generated from ssjs";
o.alertType = "info";
// The requestScope var name should match the alertName given to the bootAlert control
requestScope.put("alertDemo2",o);  
  • Finally, you can use view.postScript() to trigger a bootAlert:
// Server side js
// The alert custom control does NOT need to be part of a partial refresh target
// The parameters being passed to bootAlert need to be serialized
var o = {}
o.title = "Server Side > Client Side";
o.body = "This alert is being triggered by client side js called from server side js";
o.alertType = "warning";
o.autoClose = false;
view.postScript("bootAlert.show('alertDemo2'," + toJson(o) + ")");

I hope others find this control as useful as I have in my projects!


XPages tip: Using jQuery class selectors with hidden inputs

If you cut your XPages teeth by using the out-of-the-box-included dojo libraries, then you are probably intimately familiar with dojo.byId(“#{id:myElementID}”) to locate elements on your XPage based on the id attribute.

After starting to weave jQuery into your projects, you probably realized pretty quickly that the jQuery id selector syntax of $(“#myElementID”) does not work well with XPage component ids. But of course there is a solution for that – Marky Roden’s XSnippet which wraps the jQuery ID selector function in a XPage version.

Due to the above issues with getting elements by ID in jQuery, you probably started using class selectors such as $(“.myClassName”). This is all fine and good – until you go to add a class to the properties panel of a hidden input control:

As you can see from the screenshot above, there is no place to enter a class name in the properties for a hidden input control. Seems kind of silly, doesn’t it? It’s a field. Fields have classes. I should be able to add a class to a hidden field. Apparently not.

Luckily, there is an incredibly simple way to do this using the attrs properties in the basics section of the properties panel:

2014-08-17_23-05-43
  • Add a new attribute by clicking the plus sign
  • In the name field, enter “class” (do not add the quotes)
  • In the value field, enter the name of your class, for example, “myClassName” (again, no quotes)

Now, you should be able to access your hidden input using a jQuery selector.