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.