Friday, July 22, 2011

Update on the "enhanced" validation messages

I added functionality to select/focus a dijit tab if the field is inside a dijit.layout.TabContainer. I also added a highlight effect when a field is focused.

Source code for the custom control can be found in the original post.

Tuesday, July 19, 2011

Custom Control for "enhanced" validation messages

Update 22.06.2012: Now shows messages not bound to any control. E.g. messages related to using concurrencyMode
Update 08.06.2012: Added sorting routine to get the messages in the same order that they're in the page
Update 22.07.2011: I added functionality to select/focus a dijit tab if the field is inside a dijit.layout.TabContainer. I also added a highlight effect when a field is focused.
Disclaimer: This custom control is not entirely my idea. I've been thinking about doing something like this for a while. After I tried to help with this question by Steve Pridemore in the XPages Development Forum, I found the solution.
The code below can be used as a custom control that is a little bit more advanced (probably has its flaws) than the regular Display Errors control. If the field with a validation error has a label, it shows the label, then the error message. On the label, a link is generated that sets focus to the related field when you click it.
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">
 <xp:this.beforeRenderResponse>
  <![CDATA[#{javascript:function addChildrenClientIds(component:javax.faces.component.UIComponentBase, clientIds:java.util.ArrayList) {
 try {
  var children = component.getChildren();
  
  for (var child in children) {
   clientIds.add(child.getClientId(facesContext));
   if (child.getChildCount() > 0) {
    addChildrenClientIds(child, clientIds);
   }
  }
 } catch (e) {
  /*Debug.logException(e);*/
 }
}

try {
 var messageObjects = [];
 var messageClientIds = facesContext.getClientIdsWithMessages(); 
 
 // There are messages for components - Get client ids in sorted order
 if (messageClientIds.hasNext()) {
  var clientIds = new java.util.ArrayList();
  addChildrenClientIds(view, clientIds);
 }
 
 // Used to keep track of which messages are for components
 var componentMessages = new java.util.ArrayList();
 
 while (messageClientIds.hasNext()) {
  var clientId = messageClientIds.next();
  if( !clientId ){ continue; }
  
  var component = view.findComponent( clientId.replace( view.getClientId( facesContext ), '').replace( /\:\d*\:/g, ':') );
  if (!component) { continue; }
  
  // Fetch messages for component
  var message = '',
  messages = facesContext.getMessages( clientId );
  while (messages.hasNext()) {
   var messageItem = messages.next();
   message += (message) ? ', ' : '' + messageItem.getSummary();
   
   componentMessages.push( messageItem );
  }
  
  // If component has label - fetch
  var labelComponent = getLabelFor(component);
  var label = (labelComponent) ? labelComponent.getValue() : '';
  if (!label && component) {
   var id = component.getId();
   if (id.indexOf('_') > 0) {
    label = id;
   }
  }
  
  if (label && label.indexOf(':') === -1) {
   label += ':';
  }
  
  messageObjects.push({
   index : clientIds.indexOf(clientId),
   clientId : clientId,
   label : label,
   message : message
  });
 }
 
 // Sort message object by the order of the components in the page
 messageObjects.sort(function (a, b) {
  if (a.index > b.index) { return 1; }
  if (a.index < b.index) { return -1; }
  return 0;
 });
 
 // Add all (if any) system messages at the top
 var allMessages = facesContext.getMessages();
 while( allMessages.hasNext() ){
  messageItem = allMessages.next();  
  if( !componentMessages.contains( messageItem ) ){   
   messageObjects.unshift({ message: messageItem.getSummary() });
  }
 }
 
 viewScope.messageObjects = messageObjects;
} catch (e) { 
 /*Debug.logException(e);*/
}
}]]></xp:this.beforeRenderResponse>
 <xp:scriptBlock>
  <xp:this.value><![CDATA[var EMessages = {
 // Set focus to field
 setFocus: function( clientId ){
  var matchingFieldsByName = dojo.query('[name=' + clientId + ']');
  if (matchingFieldsByName.length > 0) {
   if (dijit && dijit.registry) {
    this.showDojoTabWithField(clientId);
   }
   var field = matchingFieldsByName[0];
   
   // Workaround for dijit fields
   if( field.getAttribute( 'type' ) === 'hidden' ){
    var matchingFieldsById = dojo.query('input[id=' + clientId + ']');
    field = matchingFieldsById[0];
   }
   
   field.focus();
   dojo.animateProperty({
    duration : 800,
    node : field,
    properties : {
     backgroundColor : {
      start : '#FFFFEE',
      end : dojo.style(field, 'backgroundColor')
     }
    }
   }).play();
  }
  return false;
 },
 
 // If field is inside a dijit/extlib TabContainer - activate
 showDojoTabWithField: function( clientId ){
  dijit.registry.byClass("extlib.dijit.TabContainer").forEach(function (tabContainer) {
   dojo.forEach(tabContainer.getChildren(), function (containerPane) {
    if ( dojo.query( containerPane.containerNode ).query( '[name="' + clientId + '"]' ).length > 0) {
     tabContainer.selectChild(containerPane);
     return;
    }
   });
  });

  dijit.registry.byClass("dijit.layout.TabContainer").forEach(function( tabContainer ){
   dojo.forEach( tabContainer.getChildren(), function( containerPane ){
    if( dojo.query( containerPane.containerNode ).query( '[name=' + clientId + ']' ).length > 0 ){
     tabContainer.selectChild( containerPane );
    }
   });
  });
 }
}]]></xp:this.value>
 </xp:scriptBlock>
 <xp:repeat id="messageRepeat" styleClass="xspMessage" rows="30" value="#{viewScope.messageObjects}" var="messageObject">
  <xp:this.rendered><![CDATA[#{javascript:return ( viewScope.messageObjects && viewScope.messageObjects.length > 0 ); }]]></xp:this.rendered>
  <xp:this.facets>
   <xp:text xp:key="header" escape="false">
    <xp:this.value><![CDATA[<ul>]]></xp:this.value>
   </xp:text>
   <xp:text xp:key="footer" escape="false">
    <xp:this.value><![CDATA[</ul>]]></xp:this.value>
   </xp:text>
  </xp:this.facets>
  <li>
   <xp:panel rendered="#{!empty(messageObject.clientId)}">
    <a href="#" onclick="return EMessages.setFocus( '#{messageObject.clientId}');">
     <xp:text escape="false">
      <xp:this.value><![CDATA[#{javascript:return (messageObject.label) ? messageObject.label : messageObject.message;
}]]></xp:this.value>
     </xp:text>
    </a>
   </xp:panel>
   <xp:text value="#{messageObject.message}" rendered="#{javascript:return (messageObject.label != '');}" />
  </li>
 </xp:repeat>
</xp:view>



The code should work with fields inside a single level repeat. I'm not sure about deeper nesting. Pop the custom control into the page like you would with the Display Errors control.

Feel free to use the code however you like. If you improve on it, please share with the community.

Wednesday, July 13, 2011

XPages: Styling required and invalid fields

Just discovered that in Domino 8.5.2 (not sure about previous releases), invalid fields get the attribute aria-invalid=true, and required fields aria-required=true.

That makes it easy to style in modern browsers (>IE6).

Simply add a couple of style rules (just an example):
[aria-required=true] { background-color: #ffe; }
[aria-invalid=true] { background-color: #fee; border-color: red; }

Valid - required fields "highlighted"


Invalid


+1 to IBM for implementing :)

Share and enjoy!

Tuesday, July 12, 2011

Small tip regarding optimizing FTSearches

Lately I've been working on SQL (MS). At work today, I had a talk about optimizing queries with a colleague more seasoned in the art of writing queries. We got into a talk about if the order of the filter statements (WHEN ..=..) and performance. Apparently, MS have optimized their engine so that the order of the filtering statements don't have much influence on the performance of the query.

This got me thinking about FTSearch. A year or so ago, I thought about doing some testing on how you could structure an FT query to get the best performance, but never got around to do it.

I did a little test today, and it seems like the order of the filters doesn't influence the result much. One thing that seems to heavily influence the result is if one of the query items alone results in a lot of documents.

If you search for "Tom", and the value "Tom" is in a field in a lot of documents, this will drag down the result, no matter if another query item in the query would result if only one document being returned from the query.

Example from test:
Searching for 'abigail AND abbott' - 2-5ms to get result.
Searching for '[Form=Person] AND abigail AND abbott' - 15-20ms to get result.

Conclusion from my test. Query items in a FTSearch that alone results in a lot of documents drags down the performance of the entire query. Order of query items doesn't seem to influence the performance of the query.

If you're building a search engine for databases with a lot of documents, try to avoid having general filters (Form/etc.) if possible.