This example came about from the desire to hightlight a Primefaces TreeTable row when a checkbox in the row is ticked. I wanted to do this because a ticked checkbox is not as visible as I would like when a lot of data is present. I was not keen on adding DataTable style dynamic selection by hovering and clicking anywhere in a row, as this conflicts somewhat with the paradigm of clicking on the expander to expand/collapse the tree, and you would have to special case hovering/clicking on the expander somehow to prevent row selection.
Ideally you would use CSS for this, but this would involve tagging a component in a TreeTable cell (<td> element) with a class (as in the TreeTable component we cannot directly add row styling), and then using some kind of ancestor selector to traverse the DOM tree upwards to get the row. The simple answer to this is that whilst CSS can do some clever things, it does not have any kind of ancestor selector, so it is not possible to do it just with CSS.
An attractive alternative for this kind of thing in Primefaces is to use jQuery – a very powerful JavaScript based UI library upon which Primefaces is based. As it is already present in a Primefaces environment, it is trivial to call and ideal for this kind of thing. In particular, it has powerful mechanisms for traversing and searching the DOM tree and selecting elements etc. If you combine this with its features for dynamically adding and removing classes from elements, it can do exactly the kind of thing that we cannot do in CSS. The jQuery website is here, and the API documentation for it may be found here.
The sample code fragments below demonstrate the idea. The sample is based on a hacked version of the the Primefaces showcase TreeTable demo. Note the following points:-
- It is perfectly possible to add both Javascript and JSF EL inline inside a JSF component, such that the JavaScript runs at that point when the page is rendered. This example adds some JavaScript inside the JSF tags for a checkbox.
- As the example uses JavaScript, the hightlight is added/removed both client side from a click event on the check box, and when the page is rendered based on the current check box value in the TreeTableDocument object from the server.
- The sample uses the JSF 2.0 EL implicit object reference component.clientId to get the client ID of the component where the EL reference is used. This is new for JSF 2.0 (previously additional code would be needed for this) and like the cc implicit object used with composite components, is not at all well documented. I was unable to find reference documentation for it anywhere in the JSF 2.0 specs, but found details of it here (at the end of the post). Note that the reference needs to be placed with care – it sometimes appears to come up with ‘ghost’ client IDs which may have been allocated by JSF but do not actually appear anywhere as IDs in the HTML. When using it, check that it returns the correct ID, for example by just placing a bare #{component.clientId} on the page directly adjacent to where you are using it, and view the source of the generated page in the browser to check that the client ID it returns matches the id of the component it is used for.
- Be careful to consider any ordering issues – the inline JavaScript call I am using must be placed on the page after the component it refers to has been rendered. By default, using component.clientId inside a script block which is in turn inside the checkbox component tags causes the script block to be output immediately prior to the html for the checkbox. Therefore, I use two script blocks – one inside the checkbox to save the component ID in a temporary JavaScript variable, which guarantees that I obtain the correct component ID and not an incorrect or ‘ghost’ one, and then another script block immediately after the closing tag for the checkbox component which passes the form ID and saved component ID to a JavaScript function which calls jQuery to do the highlighting after the html element has been rendered on the page. Note that the client side onClick() event on the checkbox will always run after the full page has been rendered, when the user clicks the box, so ordering is not an issue for this.
- The call to jQuery passes in the checkbox DOM element, which jQuery wraps with a jQuery object and returns. The parent() method is then used twice to traverse the DOM tree up from the check box to the table cell <td>, and thence to the table row <tr>. The addClass() and removeClass() methods are then used to dynamically add/remove the required CSS classes to the table row depending on the passed check box state. Adding and removing classes with jQuery is useful as it does all the string bashing for you to check if the class being added/removed is already present, handles spaces correctly, and leaves all other classes on the element intact.
- Note that in an earlier version I had tricky issues with nested quotes in JSF EL. I initially used outer double quotes, with inner single quotes on the JavaScript calls. The EL through this out however. I ended up using double quotes throughout, and escaped the inner ones with backslashes (\). This worked fine. The version shown here does not need this as it is a later modification.
- The example extends the Primefaces Document class used in the showcase, adding a boolean property for the check box value.
- My example uses CDI, whereas the original showcase uses JSF managed beans.
Sample Code Fragments
Index.xhtml
<!DOCTYPE HTML>
<html xmlns="http://www.w3c.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:fn="http://java.sun.com/jsp/jstl/functions"
xmlns:p="http://primefaces.prime.com.tr/ui"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:util="http://java.sun.com/jsf/composite/uk.co.salientsoft/util">
<f:view contentType="text/html">
<h:head>
<util:themeSwitcher outputStyle="true"
themeBase="/resources/themes"
defaultTheme="redmond"/>
<style>
.ui-widget {
font-size: 75% !important;
}
h1.header {
font-size: 1.2em !important;
padding: 5px;
margin: 0px 0px 20px 0px;
}
h2.subheader {
font-size: 0.9em !important;
padding: 2px;
margin: 0px 0px 10px 0px;
}
.float {
float: left;
}
.clear {
clear: both;
}
.themeswitcher {
float: left;
clear: both;
margin-bottom: 10px;
}
.ss-selectcolumn {
width:30px !important;
padding:0px !important;
text-align:center;
}
.ss-checkbox {
color:red!important;
}
/* treetable arrow light/dark switch
.ui-treetable tr.collapsed td .expander {
background-image:url("/TreeTable1/javax.faces.resource/treetable/images/toggle-expand-light.png.jsf?ln=primefaces&v=2.2.RC2");
}
.ui-treetable tr.expanded td .expander {
background-image:url("/TreeTable1/javax.faces.resource/treetable/images/toggle-collapse-dark.png.jsf?ln=primefaces&v=2.2.RC2");
}
*/
/* Styling nicked from default.css in the Primefaces Showcase */
a {text-decoration: none;}
a:hover {text-decoration: underline;}
a img {border: none;}
</style>
<script type="text/javascript">
//<![CDATA[
function highlightTreeTableRow(formId, checkBoxId) {
var checkBox = document.forms[formId][checkBoxId];
if (checkBox.checked)
jQuery(checkBox).parent().parent().addClass(‘ui-state-highlight ui-selected’);
else
jQuery(checkBox).parent().parent().removeClass(‘ui-state-highlight ui-selected’);
}
//]]>
</script>
</h:head>
<h:body>
<h1 class="float header ui-widget ui-widget-header ui-corner-all" style="">Theme Switcher</h1>
<h:form prependId="false" id="frmTest">
<util:themeSwitcher form="frmTest" outerStyleClass="themeswitcher" style="margin-bottom:20px;"/>
<br/><br/>
<div style="width:600px;clear:both;">
<p:treeTable value="#{documentsController.root}" var="document"
styleClass="#{(document.name==’logo.png’)? ‘ui-state-highlight ui-selected’:’fred’}"
expanded="true">
<p:column>
<f:facet name="header">
Name
</f:facet>
<h:outputText value="#{document.name}" />
</p:column>
<p:column>
<f:facet name="header">
Size
</f:facet>
<h:outputText value="#{document.size}" />
</p:column>
<p:column>
<f:facet name="header">
Type
</f:facet>
<h:outputText value="#{document.type}" />
</p:column>
<p:column>
<f:facet name="header">
Options
</f:facet>
<p:commandLink update="documentPanel" oncomplete="documentDialog.show()" title="View Detail">
<p:graphicImage value="/images/search.png"/>
<f:setPropertyActionListener value="#{document}" target="#{documentsController.selectedDocument}" />
</p:commandLink>
</p:column>
<p:column styleClass="ss-selectcolumn">
<f:facet name="header">
<h:selectBooleanCheckbox disabled="true" label="box header"/>
</f:facet>
<h:selectBooleanCheckbox value="#{document.selected}"
onclick="highlightTreeTableRow(‘frmTest’, this.id);" styleClass="ss-checkbox" id="selector" >
<script>selectId = ‘#{component.clientId}’;</script>
</h:selectBooleanCheckbox>
<script>highlightTreeTableRow("frmTest", selectId);</script>
</p:column>
</p:treeTable>
<p:commandButton type="submit" value="Submit" onclick="submit();" style="margin-top:10px;"/>
<p:dialog header="Document Detail" fixedCenter="true" effect="FADE" effectDuration="0.3"
widgetVar="documentDialog" modal="true">
<p:outputPanel id="documentPanel">
<h:panelGrid columns="2" cellpadding="5">
<h:outputLabel for="name" value="Name: " />
<h:outputText id="name" value="#{documentsController.selectedDocument.name}" style="font-weight:bold" />
<h:outputLabel for="size" value="Size: " />
<h:outputText id="size" value="#{documentsController.selectedDocument.size}" style="font-weight:bold" />
<h:outputLabel for="type" value="Type " />
<h:outputText id="type" value="#{documentsController.selectedDocument.type}" style="font-weight:bold" />
</h:panelGrid>
</p:outputPanel>
</p:dialog>
</div>
</h:form>
</h:body>
</f:view>
</html>
DocumentsController.java (modified from Showcase)
package org.primefaces.examples.view;
import java.io.Serializable;
import java.util.logging.Logger;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;
import org.primefaces.model.DefaultTreeNode;
import org.primefaces.model.TreeNode;
import uk.co.salientsoft.treetable.view.TreeTableDocument;
@Named
@SessionScoped
public class DocumentsController implements Serializable {
private static final long serialVersionUID = -3593168523028092346L;
private static final Logger logger = Logger.getLogger(DocumentsController.class.getName());
private TreeNode root;
private TreeTableDocument selectedDocument;
public DocumentsController() {
root = new DefaultTreeNode("root", null);
TreeNode documents = new DefaultTreeNode(new TreeTableDocument("Documents", "-", "Folder"), root);
TreeNode pictures = new DefaultTreeNode(new TreeTableDocument("Pictures", "-", "Folder"), root);
TreeNode music = new DefaultTreeNode(new TreeTableDocument("Music", "-", "Folder"), root);
TreeNode work = new DefaultTreeNode(new TreeTableDocument("Work", "-", "Folder"), documents);
TreeNode primefaces = new DefaultTreeNode(new TreeTableDocument("PrimeFaces", "-", "Folder"), documents);
//Documents
TreeNode expenses = new DefaultTreeNode("document", new TreeTableDocument("Expenses.doc", "30 KB", "Word Document"), work);
TreeNode resume = new DefaultTreeNode("document", new TreeTableDocument("Resume.doc", "10 KB", "Word Document"), work);
TreeNode refdoc = new DefaultTreeNode("document", new TreeTableDocument("RefDoc.pages", "40 KB", "Pages Document"), primefaces);
//Pictures
TreeNode barca = new DefaultTreeNode("picture", new TreeTableDocument("barcelona.jpg", "30 KB", "JPEG Image"), pictures);
TreeNode primelogo = new DefaultTreeNode("picture", new TreeTableDocument("logo.jpg", "45 KB", "JPEG Image"), pictures);
TreeNode optimus = new DefaultTreeNode("picture", new TreeTableDocument("optimusprime.png", "96 KB", "PNG Image"), pictures);
//Music
TreeNode turkish = new DefaultTreeNode(new TreeTableDocument("Turkish", "-", "Folder"), music);
TreeNode cemKaraca = new DefaultTreeNode(new TreeTableDocument("Cem Karaca", "-", "Folder"), turkish);
TreeNode erkinKoray = new DefaultTreeNode(new TreeTableDocument("Erkin Koray", "-", "Folder"), turkish);
TreeNode mogollar = new DefaultTreeNode(new TreeTableDocument("Mogollar", "-", "Folder"), turkish);
TreeNode nemalacak = new DefaultTreeNode("mp3", new TreeTableDocument("Nem Alacak Felek Benim", "1500 KB", "Audio File"), cemKaraca);
TreeNode resimdeki = new DefaultTreeNode("mp3", new TreeTableDocument("Resimdeki Gozyaslari", "2400 KB", "Audio File"), cemKaraca);
TreeNode copculer = new DefaultTreeNode("mp3", new TreeTableDocument("Copculer", "2351 KB", "Audio File"), erkinKoray);
TreeNode oylebirgecer = new DefaultTreeNode("mp3", new TreeTableDocument("Oyle bir Gecer", "1794 KB", "Audio File"), erkinKoray);
TreeNode toprakana = new DefaultTreeNode("mp3", new TreeTableDocument("Toprak Ana", "1536 KB", "Audio File"), mogollar);
TreeNode bisiyapmali = new DefaultTreeNode("mp3", new TreeTableDocument("Bisi Yapmali", "2730 KB", "Audio File"), mogollar);
}
public TreeNode getRoot() {return root;}
public void setRoot(TreeNode root) {this.root = root;}
public TreeTableDocument getSelectedDocument() {return selectedDocument;}
public void setSelectedDocument(TreeTableDocument selectedDocument) {
this.selectedDocument = selectedDocument;
}
}
TreeTableDocument.java
package uk.co.salientsoft.treetable.view;
import org.primefaces.examples.domain.Document;
public class TreeTableDocument extends Document {
private static final long serialVersionUID = 7339615098050125785L;
public TreeTableDocument(String name, String size, String type) {
super(name, size, type);
}
private boolean;
public boolean isSelected() {return this.selected;}
public void setSelected(boolean selected) {this.selected = selected;}
}