A TabControl with one line of code in the LightSwitch HTML Client

Posted by Xander van der Merwe on 31 July 2014 | 0 Comments

Tags:

The current Microsoft LightSwitch HTML client (or CBA app) includes top level tabs for each screen, but sometimes you need lower level tab controls to use inside those top level tabs. LightSwitch is an amazing tool and with a bit of javascript and CSS you can achieve almost anything. In this blog post I will show you how to add a simple javascript library file with a little bit of CSS which will then allow you to create tab controls like the following with one single line of javascript code in a _postRender() event (source code available for download at the bottom of this article):

LSTabControl3

 

 

 

 

 

 

 

 

 

The above was achieved by designing the nested tab control using a set of Group layout controls in the LightSwitch screen designer: one for the actual tab control and two nested ones for each of the tab pages. Here is the screen design: 

 LSTabControlDesigner

 

 

 

 

 

 

 

 

 

 

 

Do the following to add this Tab Control support to your LightSwitch project:

Add a new javascript file lsu.js (LightSwitch Utils) to you HTML Client Scripts folder, add a reference to this file in Default.htm below the reference to generatedAssets.js and put the following javascript code in that file:

var LightSwitchUtils = (function () {

// constructor
function LightSwitchUtils() {}

// C# string.Format() equivalent.
// e.g. var greeting = LSU.format("Hi, {0}", name);
LightSwitchUtils.prototype.stringFormat = function () {
var s = arguments[0];
for (var i = 0; i < arguments.length - 1; i++) {
var reg = new RegExp("\\{" + i + "\\}", "gm");
s = s.replace(reg, arguments[i + 1]);
}
return s;
};

// Render a tab strip using the .tabstrip css classes inside user-customization.css for the outer group contentItem where
// each of the immediate contentItem children will be rendered as tabs.
// Params activeTabIndex, leftMargin, rightMargin are optional and default to 0
LightSwitchUtils.prototype.renderTabStrip = function (element, contentItem, activeTabIndex, leftMargin, rightMargin) {
// optional params
activeTabIndex = (typeof activeTabIndex === "undefined") ? 0 : activeTabIndex;
leftMargin = (typeof leftMargin === "undefined") ? 0 : leftMargin;
rightMargin = (typeof rightMargin === "undefined") ? 0 : rightMargin;

// create the tabstrip HTML and add tab names to an array for re-use during click event (use the contentName as id of <ul/>)
var html = LSU.stringFormat("<ul class='tabStrip' data-value='' style='margin-left:{0}px; margin-right:{1}px;' id={2}>",
leftMargin, rightMargin, contentItem.name);
var tabNames = [];
for (var i = 0; i < contentItem.children.length; i++) {
tabNames.push(contentItem.children[i].name);
contentItem.children[i].isVisible = (activeTabIndex == i);
html += (LSU.stringFormat("<li class='{0}' id='" + contentItem.children[i].name + "'>&nbsp;" +
contentItem.children[i].displayName + "&nbsp;</li>",
activeTabIndex == i ? "selected" : ""));
};
html += "</ul>";

// render the HTML inserting it before the current element as it needs to show at the top of the contentItem group
if (navigator.userAgent.match(/MSIE/) !== null) {
$(element).css("margin-top", "-14px"); // IE 10 leaves a margin between tabs and content that needs to be removed
}
$(element).css("border-top", "1px solid #c0c0c0");
$(element).css("border-left", "1px solid #c0c0c0");
$(element).css("border-right", "1px solid #c0c0c0");
$(element).css("border-bottom", "1px solid #c0c0c0");
$(element).css("margin-left", LSU.stringFormat("{0}px", leftMargin));
$(element).css("margin-right", LSU.stringFormat("{0}px", rightMargin));
$(element).before($(html));
// add a tab click event to facilitate the tab selection
$(LSU.stringFormat("#{0} > li", contentItem.name)).click(function (e) {
// select clicked tab
var li = this;
$(LSU.stringFormat("#{0} > li", contentItem.name)).removeClass("selected");
$(li).parent().attr("data-value", $(li).text());
$(li).addClass("selected");
$(li).parent().attr("data-value", "");
// make active content visible
var selectedTab = $(li).attr('id');
setTimeout(function () {
// show selected tab and hide others
tabNames.forEach(function (tab) {
contentItem.screen.findContentItem(tab).isVisible = (tab === selectedTab);
});
});
});
};
return LightSwitchUtils;
})();

// -- create main object to use
var LSU = new LightSwitchUtils();

Add the following CSS to Content/user-customization.css file:

/* tabstrip */
.tabStrip {
text-indent:0px;
padding-left:0px;
-webkit-margin-before: 0px;
-webkit-margin-after: 0px;
}
.tabStrip li{
display:inline-block;
border:solid 1px #c0c0c0;
background-color:#F2F2F2;
color:#585858;
padding-top:7px;
padding-left:7px;
padding-right:7px;
padding-bottom:7px;
font: 10pt Arial;
cursor:pointer; cursor:pointer;
margin-top:3px;
margin-bottom:-1px;
margin-right:2px;
text-align:center;
}
.tabStrip li.selected {
background-color: white;
border-bottom:solid 1px white;
}

And here is the promised "one line of code" that goes into the _postRender() method for the TabControl Group control:

myapp.CustomerEdit.TabControl_postRender = function (element, contentItem) {
    // select the first tab by default (index = 0)
// add a left margin of 10px to left-align the tab control with the other controls above
LSU.renderTabStrip(element, contentItem, 0, 10, 0);
};

How does it work?

  1. It basically dynamically generates a set of UL (for the tab control) and LI (for the tab pages) elements and injects those dynamically at run-time just above the main Group control used as the tab control
  2. The LI elements are rendered as the "tab strip"
  3. It hides all the nested Group controls (used as tab pages) except for the currently selected on
  4. It dynamically adds a click event handler to each of the tab pages and when you click one it will make that nested Group control visible and hide all the others
  5. It paints the appropriate borders to make it look like a tab control
  6. It paints a white border line below the selected tab to hide the line below the tab strip (a bit of a visual trick)

Notes:

  1. You also get the LSU.formatString() function included in the utility library above (useful for C# programmers like me)
  2. The .renderTabControl() and included CSS only works for the LightSiwtch Light theme - you will have to change it accordingly to make it work for the dark theme
  3. The outer TabControl Group control should only contain nested Group controls to serve as the Tab pages (the Tab page Group controls may contain any type of control)
  4. You can use multiple of these tab controls on the same screen (I think I've even nested them at one point as a test)
  5. There may be bugs - please provide feedback on the MSDN LightSwitch forum if you find any or find a better way to implement this (I'm still new to javascript)
  6. Sometimes, depending on the layout of your screen, you may have to wrap the main tab control Group control into another "dummy" Group control to make everything on the screen render nicely. You will notice in the above example (also attched below) that if you change the Width of the Classification dropdown to be "Fixed Size" (e.g. 200px) as opposed to "Stretch to Container" then the tab rendering does not work nicely - in that case you have to wrap the tab control into a dummy parent Group control.
  7. The tab control works with either Column or Row Group layouts - it doesn't really matter

I would also like to give Matt Ireland credit for his ROLODEX INDEX STYLE FILTER FOR LIGHTSWITCH article that served as the initial inspiration for doing this.

Hope you find this as useful as I have. I constantly change/enhance it, so keep an eye on this blog and the MSDN LightSwitch forums for updates. Due to all the spam I'm getting I've disabled comments on this blog so please use the MSDN LightSwitch forums for feedback.

Download the VS2013 solution here (5.8MB zip): LightSwitchTabControl.zip