Sunday, April 21, 2013

Building Multiple Selection Lists

The web application that Spring Roo builds by default is restricted to modifications of a single entity.  This interface treats all objects independently, but real life objects have associations with each other.  Effectively managing those associations for the user then is essential for the applications usability.  One part of doing this is to support multiple selection of entities in the user interface, allowing the application to manage many to many relationships for the user.

Adding multiple selection of entities can be done by making a few modifications to the existing table tag.  The scope of the modifications include;
  • Add a check box for each table row.  
  • Provide the ability to turn off the show links.
  • Provide a master select box.
Adding Multi-Select Check Boxes

Adding a check box to each row of the table is central feature of the modifications being made. To do this a new optional attribute to the tag is added.  When this attribute is omitted the tag operates as it normally does, to maintain compatibility with existing usage.  However when present the tag will present a check box in a table cell at the beginning of each row.  The value of the attribute will be used as the name for the check box and the entities id will be the value.

To do this first we need to add the attribute specification to the tag;

<jsp:directive.attribute name="multiselect" 
 type="java.lang.String" required="false" rtexprvalue="false" 
 description="The name for multiselection checkboxes." />

The attribute is named 'multiselect' and when it's not empty a check box is added to the beginning of each row in the table.  But of course to keep things aligned we must also add a table cell to the heading.  This table cell should also contain a check box to function as a master select box.

<c:if test="${not empty multiselect}">
 <th >
  <input type="checkbox" name="masterSelectBox_${multiselect}" />
  <spring:eval var="colCounter" expression="colCounter  + 1" />
 </td>
</c:if>

We will use the checkbox here 'masterSelectBox_' appended with the multiselect attribute value to provide a unique name in case a page contains more then one table.  All of this of course is placed inside a conditional such that if the 'multiselect' attribute is not empty then we add the table cell.  Next we need to add the check boxes to each tow;

<c:if test="${not empty multiselect}">
 <td class="selectBox">
  <input type="checkbox" 
   name="${multiselect}" value="${item.id}" />
 </td>
</c:if>

Like with the column heading this block of JSP is placed inside a conditional, so it only appears if the multiselect attribute is defined.  The table cell is given the class 'selectBox', that will be used by Javascript later to find the cells.  And finally we have the input checkbox itself, here the name is value of the 'multiselect' attribute.

Provide the Ability to Turn Off Show Links


While the existing table tag allows turning off the update and delete links, it doesn't provide the same for the show (view) links.  We are going to need to able to remove these links to prevent the user from exiting the page during the middle of making selections.

To implement this first we need to add another attribute directive to the tag.

<jsp:directive.attribute name="show" 
 type="java.lang.Boolean" required="false" rtexprvalue="true" 
 description="Include 'show' link into table (default true)" />


Then place a conditional expression around the existing table heading cell.

<c:if test="${show}">
 <th></th>
 <spring:eval var="colCounter" expression="colCounter  + 1" />
</c:if>

And finally place a conditional around the existing table cell containing the show link.

<c:if test="${show}">
 <td class="utilbox">
  <spring:url value="${path}/${itemId}" var="show_form_url" />
  <spring:url value="/resources/images/show.png" var="show_image_url" />
  <spring:message arguments="${typeName}" code="entity_show" var="show_label" htmlEscape="false" />
  <a href="${show_form_url}" alt="${fn:escapeXml(show_label)}" title="${fn:escapeXml(show_label)}">
   <img alt="${fn:escapeXml(show_label)}" class="image" src="${show_image_url}" title="${fn:escapeXml(show_label)}" />
  </a>
 </td>
</c:if>     

Provide a Master Select


Most applications that support multiple selection of list also provide a means to select or deselect all the entries on the list.  In the first section, 'Adding Multi-Select Check Boxes' we added a master select check box to the table heading cell.  So all that is left to do is adding some Javascript to provide that functionality.

<c:if test="${not empty multiselect}">
 <script>
  require( [ "dojo/query", "dojo/on", "dojo/domReady!" ], function( query, on ) {
   var masterSelect = null ;
       
   query( "table#${id} input.masterSelectBox_${multiselect}" ).forEach( function( node ){
    masterSelect = node ;
    on( node, "click", function( e ) {
     query( "table#${id} td.selectBox input[type=checkbox]").forEach( function( node ) {
      node.checked = masterSelect.checked ;
     }) ;
    })
   })
  }) ;
  
 </script>
</c:if>

Here a Dojo query is used to find the master select box that was added to the table in the first stage.  A 'chick' event handler is added to that check box.  When clicked another Dojo query is executed to find the check box associated with each entity.  The check boxes discovered from that query are then set to the current state of the master select check box.

Putting It All Together


Now that all the pieces are in place, here's the complete modified table tag code;

<jsp:root xmlns:c="http://java.sun.com/jsp/jstl/core" xmlns:fn="http://java.sun.com/jsp/jstl/functions" 
  xmlns:util="urn:jsptagdir:/WEB-INF/tags/util" xmlns:spring="http://www.springframework.org/tags" xmlns:form="http://www.springframework.org/tags/form" xmlns:fmt="http://java.sun.com/jsp/jstl/fmt" xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0">
  <jsp:directive.tag import="java.util.ArrayList" />
  <jsp:output omit-xml-declaration="yes" />

  <jsp:directive.attribute name="id" type="java.lang.String" required="true" rtexprvalue="true" description="The identifier for this tag (do not change!)" />
  <jsp:directive.attribute name="data" type="java.util.Collection" required="true" rtexprvalue="true" description="The collection to be displayed in the table" />
  <jsp:directive.attribute name="path" type="java.lang.String" required="true" rtexprvalue="true" description="Specify the URL path" />
  <jsp:directive.attribute name="typeIdFieldName" type="java.lang.String" required="false" rtexprvalue="true" description="The identifier field name for the type (defaults to 'id')" />
  <jsp:directive.attribute name="multiselect" type="java.lang.String" required="false" rtexprvalue="false" description="The name for multiselection checkboxes." />
  <jsp:directive.attribute name="show" type="java.lang.Boolean" required="false" rtexprvalue="true" description="Include 'show' link into table (default true)" />
  <jsp:directive.attribute name="create" type="java.lang.Boolean" required="false" rtexprvalue="true" description="Include 'create' link into table (default true)" />
  <jsp:directive.attribute name="update" type="java.lang.Boolean" required="false" rtexprvalue="true" description="Include 'update' link into table (default true)" />
  <jsp:directive.attribute name="delete" type="java.lang.Boolean" required="false" rtexprvalue="true" description="Include 'delete' link into table (default true)" />
  <jsp:directive.attribute name="render" type="java.lang.Boolean" required="false" rtexprvalue="true" description="Indicate if the contents of this tag and all enclosed tags should be rendered (default 'true')" />
  <jsp:directive.attribute name="z" type="java.lang.String" required="false" description="Used for checking if element has been modified (to recalculate simply provide empty string value)" />

  <c:if test="${empty render or render}">

    <c:set var="columnProperties" scope="request" />
    <c:set var="columnLabels" scope="request" />
    <c:set var="columnMaxLengths" scope="request" />
    <c:set var="columnTypes" scope="request" />
    <c:set var="columnDatePatterns" scope="request" />

    <jsp:doBody />

    <c:if test="${empty typeIdFieldName}">
      <c:set var="typeIdFieldName" value="id" />
    </c:if>

    <c:if test="${empty update}">
      <c:set var="update" value="true" />
    </c:if>

    <c:if test="${empty delete}">
      <c:set var="delete" value="true" />
    </c:if>

    <spring:message var="typeName" code="menu_item_${fn:toLowerCase(fn:split(id,'_')[fn:length(fn:split(id,'_')) - 1])}_new_label" htmlEscape="false" />
    <c:set var="lengths" value="${fn:split(columnMaxLengths, '✏')}" scope="request" />
    <c:set var="types" value="${fn:split(columnTypes, '✏')}" scope="request" />
    <c:set var="patterns" value="${fn:split(columnDatePatterns, '✏')}" scope="request" />
    
    <c:if test="${not empty multiselect}">
     <script>
      require( [ "dojo/query", "dojo/on", "dojo/domReady!" ], function( query, on ) {
       var masterSelect = null ;
       
       query( "table#${id} input.masterSelectBox_${multiselect}" ).forEach( function( node ){
        masterSelect = node ;
        on( node, "click", function( e ) {
         query( "table#${id} td.selectBox input[type=checkbox]").forEach( function( node ) {
          node.checked = masterSelect.checked ;
         }) ;
        })
       })
      }) ;
      
     </script>
    </c:if>

    <spring:eval var="colCounter" expression="0" />

    <table id="${id}">
      <thead>
        <tr>
          <c:if test="${not empty multiselect}">
            <th >
    <input class="masterSelectBox_${multiselect}" type="checkbox" name="${column}" />
            </th>
            <spring:eval var="colCounter" expression="colCounter  + 1" />
          </c:if>
          <c:forTokens items="${columnLabels}" delims="${'✏'}" var="columnHeading">
            <th>
             <c:out value="${columnHeading}" />
               <spring:eval var="colCounter" expression="colCounter  + 1" />
            </th>
          </c:forTokens>
          <c:if test="${show}">
            <th></th>
            <spring:eval var="colCounter" expression="colCounter  + 1" />
          </c:if>
          <c:if test="${update}">
            <th></th>
            <spring:eval var="colCounter" expression="colCounter  + 1" />
          </c:if>
          <c:if test="${delete}">
            <th></th>
            <spring:eval var="colCounter" expression="colCounter  + 1" />
          </c:if>
        </tr>
      </thead>
      <c:forEach items="${data}" var="item">
        <tr>
          <c:if test="${not empty multiselect}">
            <td class="selectBox">
           <input type="checkbox" name="${multiselect}" value="${item.id}" />
          </td>
          </c:if>
          <c:forTokens items="${columnProperties}" delims="${'✏'}" var="column" varStatus="num">
            <c:set var="columnMaxLength" value="${lengths[num.count-1]}" />
            <c:set var="columnType" value="${types[num.count-1]}" />
            <c:set var="columnDatePattern" value="${patterns[num.count-1]}" />
            <td>
          <c:choose>
            <c:when test="${columnType eq 'date'}">
               <spring:escapeBody>
                 <fmt:formatDate value="${item[column]}" pattern="${fn:escapeXml(columnDatePattern)}" var="colTxt" />
               </spring:escapeBody>
            </c:when>
            <c:when test="${columnType eq 'calendar'}">
               <spring:escapeBody>
                 <fmt:formatDate value="${item[column].time}" pattern="${fn:escapeXml(columnDatePattern)}" var="colTxt"/>
               </spring:escapeBody>
            </c:when>
            <c:otherwise>
               <c:set var="colTxt">
                 <spring:eval expression="item[column]" htmlEscape="false" />
               </c:set>
            </c:otherwise>
          </c:choose>
          <c:if test="${columnMaxLength ge 0}">
            <c:set value="${fn:substring(colTxt, 0, columnMaxLength)}" var="colTxt" />
          </c:if>
          <c:out value="${colTxt}" />
            </td>
          </c:forTokens>
          <c:set var="itemId"><spring:eval expression="item[typeIdFieldName]"/></c:set>
          <c:if test="${show}">
           <td class="utilbox">
             <spring:url value="${path}/${itemId}" var="show_form_url" />
             <spring:url value="/resources/images/show.png" var="show_image_url" />
             <spring:message arguments="${typeName}" code="entity_show" var="show_label" htmlEscape="false" />
             <a href="${show_form_url}" alt="${fn:escapeXml(show_label)}" title="${fn:escapeXml(show_label)}">
               <img alt="${fn:escapeXml(show_label)}" class="image" src="${show_image_url}" title="${fn:escapeXml(show_label)}" />
             </a>
           </td>
   </c:if>     
          <c:if test="${update}">
            <td class="utilbox">
              <spring:url value="${path}/${itemId}" var="update_form_url">
                <spring:param name="form" />
              </spring:url>
              <spring:url value="/resources/images/update.png" var="update_image_url" />
              <spring:message arguments="${typeName}" code="entity_update" var="update_label" htmlEscape="false" />
              <a href="${update_form_url}" alt="${fn:escapeXml(update_label)}" title="${fn:escapeXml(update_label)}">
                <img alt="${fn:escapeXml(update_label)}" class="image" src="${update_image_url}" title="${fn:escapeXml(update_label)}" />
              </a>
            </td>
          </c:if>
          <c:if test="${delete}">
            <td class="utilbox">
              <spring:url value="${path}/${itemId}" var="delete_form_url" />
              <spring:url value="/resources/images/delete.png" var="delete_image_url" />
              <form:form action="${delete_form_url}" method="DELETE">
                <spring:message arguments="${typeName}" code="entity_delete" var="delete_label" htmlEscape="false" />
                <c:set var="delete_confirm_msg">
                  <spring:escapeBody javaScriptEscape="true">
                    <spring:message code="entity_delete_confirm" />
                  </spring:escapeBody>
                </c:set>
                <input alt="${fn:escapeXml(delete_label)}" class="image" src="${delete_image_url}" title="${fn:escapeXml(delete_label)}" type="image" value="${fn:escapeXml(delete_label)}" onclick="return confirm('${delete_confirm_msg}');" />
                <c:if test="${not empty param.page}">
                  <input name="page" type="hidden" value="1" />
                </c:if>
                <c:if test="${not empty param.size}">
                  <input name="size" type="hidden" value="${fn:escapeXml(param.size)}" />
                </c:if>
              </form:form>
            </td>
          </c:if>
        </tr>
      </c:forEach>
      <tr class="footer">
        <td colspan="${colCounter}">
          <c:if test="${empty create or create}">
            <span class="new">
              <spring:url value="${path}" var="create_url">
                <spring:param name="form" />
              </spring:url>
              <a href="${create_url}">
                <spring:url value="/resources/images/add.png" var="create_img_url" />
                <spring:message arguments="${typeName}" code="global_menu_new" var="add_message" htmlEscape="false" />
                <img alt="${fn:escapeXml(add_message)}" src="${create_img_url}" title="${fn:escapeXml(add_message)}" />
              </a>
            </span>
            <c:out value=" " />
          </c:if>
          <c:if test="${not empty maxPages}">
            <util:pagination maxPages="${maxPages}" page="${param.page}" size="${param.size}" />
          </c:if>
        </td>
      </tr>
    </table>

  </c:if>

</jsp:root>


Using the Modified Table Tag


To use the modified table tag we just need to provide values for the 'multiselect' and 'show' optional attributes.

<table:table data="${organizations}" id="l_com_repik_multitenant_security_domain_Organization" 
  path="/organizations" create="false" update="false" delete="false" 
  multiselect="organizationId" show="false" 
  z="5U3sseAfezAakwxsdptI+SXks8o=">
 <table:column id="c_com_repik_multitenant_security_domain_Organization_name" property="name" z="8FaKf+BmDqidSHXdwfn1tV1TnQo="/>
 <table:column id="c_com_repik_multitenant_security_domain_Organization_description" property="description" z="2tWVqb2uDhtkEVsFS0M2anZ68vg="/>
</table:table>

In this snippet of JSP we have a multi-select table showing the organizations that a user is associated with.  Elsewhere in the code there is a JPQL query that is building that list for use here.  Here the ability for the use to modify or view individual organizations have been turned off by setting the 'create', 'update', 'delete' and 'show' attributes to false.

And now here is the mutli-select table embedded in a form;





2 comments:

  1. How I can get it, if I want to map the checkbox value to a boolean object's property?
    Also, I'm getting a javaScript error in the require function. It says require function is not defined.
    I hope you can help me out. Thanks!

    ReplyDelete