Showing posts with label code snippet. Show all posts
Showing posts with label code snippet. Show all posts

Tuesday, October 8, 2013

Configuring input field for DateTime picker with localized date/time format

In a new application a colleague of mine is working on, he wanted to let the user set date and time with Norwegian date and time format.

One would think it should be pretty straightforward, but it took me around three hours to figure out how to do it. I accidentally discovered the way to do it while making a custom control with one field for date and one field for time. The custom control was meant to combine the two values into a date/time value. Luckily there's a simpler solution.

Source for Norwegian Date/Time picker:
<xp:inputText id="dateTimePicker" value="#{document.date_time}">
 <xp:this.converter>
  <xp:convertDateTime pattern="dd.MM.yyyy HH:mm" type="both" />
 </xp:this.converter>
 <xp:dateTimeHelper>
  <xp:this.dojoAttributes>
   <xp:dojoAttribute name="constraints" value="{ datePattern : 'dd.MM.yyyy', locale: 'no', timePattern: 'HH:mm' }" />
  </xp:this.dojoAttributes>
 </xp:dateTimeHelper>
</xp:inputText>

I tested/this should work on Domino 8.5.3 and Domino 9.

Wednesday, July 3, 2013

Workaround for issues with XPage in iframe on external website

Update, 03.03.14: We had some further issues with Chrome and form submission. Code snippet updated with workaround.

A colleague of mine had some issues with an XPage running in an iframe on a customer's website. The customer notified us that some users with Chrome/Safari had issues with the page going blank.

When doing partial refresh, the page went into a reload loop. I did a quick check, and couldn't find anything wrong with the code. I believe it has something to do with cross domain cookies and Webkit.

My workaround was to have a script block that adds the SessionID parameter that was previously added in certain circumstances on earlier versions of XPages. The script block is only loaded for Chrome/Safari, and the code only runs when the XPage is in a frameset/iframe.

<xp:scriptBlock>
 <xp:this.loaded><![CDATA[${javascript:return ( context.getUserAgent().isChrome() || context.getUserAgent().isSafari() );}]]></xp:this.loaded>
 <xp:this.value><![CDATA[(function(){
// Fix for safari/chrome when page is in iframe. Cookie with SessionID seems to be discarded between requests
// Workaround: Set parameter for SessionID
var queryString = document.location.search;
var sessionIdParameter = 'SessionID=' + '#{javascript:return facesContext.getExternalContext().getRequest().getSession().getId();}';
if( self !== parent && queryString.indexOf( 'SessionID' ) === -1 ){
 if( queryString === '' ){
  document.location.search = sessionIdParameter;  
 } else {
  document.location.search = queryString + '&' + sessionIdParameter;
 } 
}

// Add SessionID parameter to form action if set in URL to preserve session
if( queryString.indexOf( 'SessionID' ) !== -1 ){ 
 var form = document.forms[0];
 if( !form ){
  return;
 }
 
 var formAction = form.action;
 var parameterDelimiter = '&';
 if( formAction.indexOf( '?' ) === -1 ){
  parameterDelimiter = '?';
 }
 form.action = form.action + parameterDelimiter + sessionIdParameter;  
}
})();]]></xp:this.value>
</xp:scriptBlock>

Wednesday, March 20, 2013

Fix for partial refresh on Dojo Tab Container/Content Pane

I wrote this a while back, but I couldn't find that I'd shared it.

You need to use the partial refresh hijacker to use the code snippet.

This code snippet initializes Dojo Tab Containers/Dojo Content Panes in the area that's refreshed:
// Fix problem with partial refresh on Dojo Tab Container/Content pane
// Source for inspiration:
// http://www.openntf.org/projects/pmt.nsf/0/D228115FAA98DDEC86257A7D0050E7FF
dojo.subscribe( 'partialrefresh-complete', this, function( method, form, refreshId ) {
 var tabContainersAndContentPanes = dojo.query( '[id=' + refreshId + '] .dijitTabContainer[widgetid], ' +
  '[id=' + refreshId + '] .dijitContentPane[widgetid]' );
 if( tabContainersAndContentPanes.length === 0 ) {
  return;
 }
 
 for( var i = 0; i < tabContainersAndContentPanes.length; i++ ) {
  var widgetId = tabContainersAndContentPanes[i].getAttribute( 'widgetid' );
  var widget = dijit.byId( widgetId );
  if( widget ) {
   widget.startup();
   widget.resize();
  }
 }
} );

Add the code snippet in a dojo.addOnLoad function or something similar.

If you have other widget types that fails to initialize, just modify the selector/startup commands to fix.

Friday, January 18, 2013

Small LS class that can be used to check if fields have changed

We've had some issues with semaphore locks on one of our import databases. The import database has routines that import/update data, then replicate it to a cluster when it's done.

We're not exactly sure what triggers the locks, but the server crashed sometimes several times a day, so we decided to see if the import routines could be optimized to do as few writes as possible.

Several of the routines saved documents even if there were no field changes. I wrote a simple class to test for field value changes.
'// Used to test if certain fields have changed
Class FieldChangeChecker
 Private fieldsToCheck As Variant
 Private fieldValues List As Variant
 
 '// Run this after calculcation/etc. to see if specified fields have changed
 Function haveFieldsChanged( doc As NotesDocument ) As Boolean
  On Error GoTo bubbleError
  
  If doc.isNewNote Then
   haveFieldsChanged = True
   Exit Function
  End If
  
  Dim initialValue As Variant, currentValue As Variant
  ForAll fieldName In Me.fieldsToCheck
   initialValue = Me.fieldValues( fieldName )
   currentValue = doc.getItemValue( fieldName )(0)
   
   If initialValue <> currentValue Then
    haveFieldsChanged = True
    Exit Function
   End If
  End ForAll
  
  Exit Function
bubbleError:
  Error Err, errorMessage()
 End Function
 
 '// Comma separate field names. E.g. "forename,surname"
 Sub New( ByVal commaSeparatedFieldNames As String )
  On Error GoTo bubbleError
  
  Me.fieldsToCheck = FullTrim( Split( commaSeparatedFieldNames, "," ) )
  
  Exit Sub
bubbleError:
  Error Err, errorMessage()
 End Sub
 
 
 '// Run this before calculation/etc.
 Sub readInitialValues( doc As NotesDocument )
  On Error GoTo bubbleError
  
  ForAll fieldName In Me.fieldsToCheck
   Me.fieldValues( fieldName ) = doc.getItemValue( fieldName )(0)
  End ForAll

  Exit Sub
bubbleError:
  Error Err, errorMessage()
 End Sub 
End Class
Example of use:
..
Dim fieldChangeChecker As New FieldChangeChecker( "FirstName,LastName,Address,PostCode,City" )
Set customerDoc = customersView.getFirstDocument()
While Not customerDoc Is Nothing
 Call fieldChangeChecker.readInitialValues( customerDoc )

 '// Code that looks up the newest information and sets fields

 If fieldChangeChecker.haveFieldsChanged( customerDoc ) Then
  Call dataDoc.Save(True, False) 
 End If

 Set customerDoc = customersView.getNextDocument( customerDoc )
Wend
..
Regarding the errorMessage-function in the class. I use error bubbling in all my LS code. If something fails somewhere in a routine, I want the routine to stop executing. At the top of the stack, I use OpenLog/LogError. This gives me a nice pseudo stack of the function calls.
Code for errorMessage function:
Function errorMessage As String
 '// Simple function to generate more readable errors when dealing with error-bubbling
 Dim message As String
 message = Error
 
 If CStr( GetThreadInfo(10) ) = "INITIALIZE" Then
  errorMessage =  "Error " & Err & " on line " & Erl & " in function " & GetThreadInfo( 10 ) & ": " + Error
 Else
  errorMessage =  Chr(13) + Chr(9) + "Error " & Err & " on line " & Erl & " in function " & GetThreadInfo( 10 ) & ": " + Error$ 
 End If 
End Function

Friday, October 12, 2012

Code Snippet - Close dialog if all fields are valid

Today I was working on a dialog that had fields with validation. I only want to close the dialog if all fields are valid. I'm not aware of any inbuilt XSP methods that does this. This code snippet checks for any invalid fields in the dialog. If all fields are valid, the dialog is closed.
function closeDialogIfValid( dialogId ){
 var invalidCount = dojo.query( '[id="' + dialogId + '"] [aria-invalid="true"]' ).length;
 if( invalidCount === 0 ){
  XSP.closeDialog( dialogId );
 }
}
This probably only works if you're using server side validation.

If there is something like this in the XSP API, please let me know.

Share and enjoy!

Thursday, September 6, 2012

Snippet to clear session for user

During testing, I sometimes log in as different user to test hide/whens/etc. I used to delete the SessionID cookie in the browser to clear session scoped beans/sessionScope variables. Today, I looked for a solution to automate this. This line will clear all objects related to a session:
facesContext.getExternalContext().getSession( false ).invalidate();
If you want to clear session when logged in user changes for the current "XPages" session, here's the snippet I use (put the code in afterPageLoad or beforePageLoad):
// Reset session when user changes
var currentUserName = sessionScope.currentUserName;
var userName = session.getEffectiveUserName();
if( currentUserName && userName !== currentUserName ){
 facesContext.getExternalContext().getSession( false ).invalidate();
}   
sessionScope.currentUserName = userName;
I would think this is better than deleting the cookie, as the server is immediately notified that it should flush the objects bound to the session. When I delete the cookie, I believe that the objects are kept in memory until a specified timeout.

Tuesday, September 27, 2011

Indicator for all partial refreshes

Sometimes partial updates take a while. To make users aware of updates happening, I made a small JS object that automatically shows a dojox.widget.Standby over the area being updated. Initially I thought that it would be too much, showing the mask over every refreshed area. So far, I quite like the effect.

The app isn't in production yet, so I don't know how users will react, but hopefully they will appreciate being made aware of that things are happening.

To load the object, put this in a JavaScript library (client side)
dojo.addOnLoad(function(){ new StandbyWidget(); });
The default background color is bright yellow. To override, simply put a hex string in the "constructor" call.
dojo.addOnLoad(function(){ new StandbyWidget( '#555'); }); // Dark grey
To use the code snippet below, you also need the partial event hijacker
var StandbyWidget = function( backgroundColor ){
dojo.require( 'dojox.widget.Standby' );
this.widget = new dojox.widget.Standby();
this.widget.attr( 'color', backgroundColor || '#ffe' );

document.body.appendChild( this.widget.domNode );
this.widget.startup();

dojo.subscribe( 'partialrefresh-init', this, function( method, form, targetId ){
if( targetId && targetId !== '@none' ){ this.show( targetId ); }
});

dojo.subscribe( 'partialrefresh-complete', this, function( method, form, targetId ){
if( targetId && targetId !== '@none' ){ this.hide(); }
});

dojo.subscribe( 'partialrefresh-error', this, function( method, form, targetId ){
if( targetId && targetId !== '@none' ){ this.hide(); }
});
}

StandbyWidget.prototype = {
show: function( targetId ){
this.widget.attr( 'target', targetId );
this.widget.show();
},
hide: function(){
this.widget.hide();
}
}

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.

Monday, June 6, 2011

Simple helper function to get path to an application

I use this when generating URLs in SSJS. The reason that protocol/hostname is added is so that the generated urls work with xp:link.

If an xp:link url starts with /, it generates the path to the current application at the start of the url. If you want to link to another app/page on the current server you either have to make an html link (<a>), hard code the path, or use something like this function.

// Returns absolute path (including protocol/hostname) to the current application
// or specified [database]
function getAppPath( db:NotesDatabase ){
try {
var currentUrl = context.getUrl();
var hostname = currentUrl.getHost();
var protocol = currentUrl.getScheme();

db = db || database;
return protocol + '://' + hostname + '/' + db.getFilePath().replace( /\\/g, '/' );
} catch( e ){ /* Exception handling */ }
}

Monday, May 16, 2011

Force embedded views to open on the same server as the app containing them

I've been doing some Notes development lately. I've noticed that the embedded views are being loaded from the server where the app that have the embedded views was opened last. I wrote a little procedure that you can run on PostOpen in database script.

Input parameter: the name(s) of the forms with external embedded views.

The code inspects the DXL of the form and fetches all the replicaids of the external embedded views. It then tries to open these databases and add them to the workspace with the server the user is currently on. As far as I've tested, it seems to work as described. Let me know if there are bugs.

Sub loadEmbeddedViewsOnCurrentServer( formsWithEmbedded As Variant )
On Error GoTo bubbleError
'// Goes through the form(s) specified by name in formsWithEmbedded and opens the dbs
'// that have the embedded views on the same server as the code is running
If DataType( formsWithEmbedded ) = 8 Then
formsWithEmbedded = Split( formsWithEmbedded, "¤¤¤" )
End If

Dim s As New NotesSession, db As NotesDatabase, currentServer As String
Set db = s.currentDatabase
currentServer = db.server

'// Find forms
Dim noteCol As NotesNoteCollection
Set noteCol = db.createNoteCollection( False )
noteCol.selectForms = True
noteCol.selectionFormula = |$title="| + Join( formsWithEmbedded, |":"| ) + |"|
Call noteCol.buildCollection()

Dim formNoteId As String, formDoc As NotesDocument
Dim dxlExporter As NotesDXLExporter, dxlStream As NotesStream
Dim embeddedPosition As String, embeddedView As String
Dim formDxl As String, embeddedViews As Variant, replicaid As String
Dim workspace As New NotesUIWorkspace(), embeddedViewDb As NotesDatabase
Dim openedDbs As Variant, result As Variant

openedDbs = Split( "" )
Set dxlStream = s.createStream()

formNoteId = noteCol.getFirstNoteId()
While formNoteId <> ""
Set formDoc = db.getDocumentById( formNoteId )

'// Extract DXL from form
Set dxlExporter = s.createDxlExporter( formDoc, dxlStream )
Call dxlExporter.process()

dxlStream.position = 0
formDxl = dxlStream.readText

'// Get embedded view info
embeddedPosition = InStr( formDxl, "<embeddedview" )
While embeddedPosition > 0
'// Embedded views can be defined as <embeddedview /> or <embeddedview></embeddedview> - try both
embeddedView = StrLeftBack( StrRightBack( formDxl, "<embeddedview" ), "</embeddedview>" )
If embeddedView = "" Then embeddedView = StrLeftBack( StrRightBack( formDxl, "<embeddedview" ), "/>" )

'// Open databases
If InStr( embeddedView, "database" ) > 0 Then
replicaid = Strtoken( StrRightBack( embeddedview, "database='" ), "'", 1 )
If replicaid <> "" Then
'// If db hasn't been opened before in the script - open
If IsNull( ArrayGetIndex( openedDbs, replicaid ) ) Then
Set embeddedViewDb = New NotesDatabase( "", "" )
Call embeddedViewDb.openByReplicaId( currentServer, replicaid )
If Not embeddedViewDb Is Nothing Then
If embeddedViewDb.isOpen Then
Call workspace.addDatabase( currentServer, embeddedViewDb.filePath )
End If
End If

openedDbs = ArrayAppend( openedDbs, replicaid )
End If
End If
End If

'// Remove start tag for the processed embedded view - only run once per embedded
formDxl = Replace( formDxl, "<embeddedview" + embeddedView, "" )

'// Find next embedded view
embeddedPosition = InStr( formDxl, "<embeddedview" )
Wend

Call dxlStream.truncate()
formNoteId = noteCol.getNextNoteId( formNoteId )
Wend

Exit Sub
bubbleError:
Error Err, Error
End Sub

Monday, November 8, 2010

Enable enhanced HTML generation via LotusScript

I have about 850 applications that now needs enhanced HTML generation enabled. As far as I know, the only way to enable this in an application is via LS or manually.

Here's example code for enabling it with LS for one db:
Dim s As New NotesSession, db As NotesDatabase
Set db = s.currentDatabase

'// Fetch the db icon document
Dim col As NotesNoteCollection, doc As NotesDocument
Set col = db.createNoteCollection( False )
col.selectIcon = True
Call col.buildCollection()

'// Enable enhanced HTML
Set doc = db.getDocumentById( col.getFirstNoteId() )
Call doc.replaceItemValue( "$AllowPost8HTML", "1" )
Call doc.save( True, False )
I found the solution here.

Friday, October 22, 2010

XPages: Make categorized views behave

Update 22.05.14: Updated with improved code.

The current implementation of categorized views gives a table column to each column. In most cases, you probably don't want this. The code below makes the categories look more like nested sections.

The code is requires Dojo 1.4, which is bundled with Domino 8.5.2. To make it compatible with previous versions, you'd have to exchange categoryButtons.closest( 'tr' ) with code that walks up the DOM tree until it finds a tr-node.

function transformCategorizedViews(){
 // Needed for dojoj.NodeList.closest()
 dojo.require("dojo.NodeList-traverse");
  
 var dataTables = dojo.query( '.xspDataTable' );
 dataTables.forEach( function( dataTable ){
  var numColumnsTotal = dojo.query( 'thead th', dataTable ).length;
   
  var categoryButtons = dojo.query( '.xspDataTable button[title~=collapsed], .xspDataTable button[title~=expanded]' );
  
  // Get parent row/find all the empty cells before expand/collapse button
  var categoryButtonRows = categoryButtons.closest( 'tr' );
  categoryButtonRows.forEach( function( categoryButtonRow ){
   var numCellsWithContent = 0;
   var numEmptyCellsBeforeButton = 0;
   var cells = categoryButtonRow.cells;
   if( cells.length > 1 ){
    var categoryButtonCell = null;
    for( var i = 0, numCells = cells.length; i < numCells; i++ ){
     var cell = cells[i];
     var cellIsEmpty = ( cell.innerHTML === '' );
     
     // Hide empty cells/set colspan to the category equal to hidden cells
     if( !categoryButtonCell ){
      if( cellIsEmpty ){
       numEmptyCellsBeforeButton += 1;        
      } else {
       categoryButtonCell = cell;
      }
     }
     
     if( cellIsEmpty ){
      cell.style.display = 'none';       
     } else {
      numCellsWithContent += 1;
     }
    }
     
    // colspan = column with expand/collapse + column count - num cells with content 
    categoryButtonCell.setAttribute( 'colspan', 1 + numColumnsTotal - numCellsWithContent );
    categoryButtonCell.style.paddingLeft = (30 * numEmptyCellsBeforeButton ) + 'px';     
   }
  });
 });
}

Since categorized views use partial refresh, you'll need something like my partial refresh hijacker.

Here's how to use the above code with the hijacker:
dojo.addOnLoad(function(){
 // Run on load
 transformCategorizedViews();
 // Run on partial refreshes
 dojo.subscribe( 'partialrefresh-complete', transformCategorizedViews );
});
Share and enjoy!

Tuesday, October 19, 2010

XPages: Show validation errors for multiple/specified components

There's a lot of great stuff in XPages, but there's also a few things I miss. One of those things is message-controls that you can connect to multiple fields. It's quite easy to emulate this using a little bit of SSJS, and a xp:text.

SSJS:
// Fetch messages for specified components
function getFacesMessages( components ){
try {
if( typeof components !== 'array' && typeof components != 'object' ){ components = [ components ]; }
var clientId, component, messages = [], msgIterator;
for( var i = 0; i < components.length; i++ ){
component = components[i];
if( typeof component === 'string' ){
clientId = getClientId( component );
} else {
clientId = component.getClientId( facesContext );
}

msgIterator = facesContext.getMessages( clientId );
if( !msgIterator ){ continue; }
while( msgIterator.hasNext() ){
messages.push( msgIterator.next().getSummary() );
}
}
return messages;
} catch( e ){ /*Debug.logException( e );*/ }
}

XPages source code example:
<xp:text styleClass="xspMessage" 
escape="false" rendered="#{javascript:return ( this.value );}">
<xp:this.value>
<![CDATA[#{javascript:return getFacesMessages( [ 'field1', 'field2' ] ).join( '<br />' );}]]>
</xp:this.value>
</xp:text>
Share and enjoy!

Thursday, October 7, 2010

XPages: Add global/field message

If you want to set a message in a xp:message/xp:messages control, here's a little code snippet for you.

// Sets a global message/message for a field
function addFacesMessage( message, component ){
try {
if( typeof component === 'string' ){
component = getComponent( component );
}

var clientId = null;
if( component ){
clientId = component.getClientId( facesContext );
}

facesContext.addMessage( clientId,
new javax.faces.application.FacesMessage( message ) );
} catch(e){ /*Debug.logException(e);*/ }
}


If the second parameter isn't specified, the message becomes global (visible in all rendered xp:messages controls on the page).

If you specify the second parameter, it has to be the component id of a field which has a xp:message control, or the field component. The message then shows up in the message control for the field.

Code stolen/ported from this page.

Share and enjoy!

Wednesday, October 6, 2010

XPages: Bug in fromJson (with fix)

Update 07.10.10: I added a by-value copy method

toJson can convert most JavaScript objects to JSON. fromJson can not convert back all JSON strings that toJson creates.

Example:
var arrayJson = toJson( [1,2,3] ); // "[1,2,3]"
fromJson( arrayJson ) // fails
If you try the same in Firefox, which has implemented the JSON API, everything works:
var arrayJson = JSON.stringify( [1,2,3] ); // "[1,2,3]"
JSON.parse( arrayJson ) // [1,2,3]
I've created a simple wrapper-class that works around this bug:
var JSON = {
// Makes a by-value copy of the object
copy: function( object ){
try {
// Faster way to copy arrays
if( object && typeof object.concat === 'function' ){ return object.concat(); }

return this.parse( this.stringify( object ) );
} catch( e ){ /*Debug.logException( e );*/ }
},

// Converts object to JSON string
stringify: function( object ){
try {
return toJson( object );
} catch( e ){ /*Debug.exception( e );*/ }
},

// Parses JSON to JS object
parse: function( JSON ){
try {
return fromJson( '{"values":' + JSON + '}' ).values;
} catch( e ){ /*Debug.exception( e );*/ }
}
}
It saddens me that there are so many "simple" bugs in the XPages API. :\

Share and enjoy!

Code snippet - Array.splice according to ECMA

I've really missed the proper Array.splice in SSJS. To avoid writing a loop for every time I want to do a "splice operation", I've made a function that should work according to spec.

// $splice -> Array.splice according to ECMA standards
function $splice( array, startIndex, numItems ){
try {
var endIndex = startIndex + numItems;
var itemsBeforeSplice = [], splicedItems = [], itemsAfterSplice = [];
for( var i = 0; i < array.length; i++ ){
if( i < startIndex ){ itemsBeforeSplice.push( array[i] ); }
if( i >= startIndex && i < endIndex ){ splicedItems.push( array[i] ); }
if( i >= endIndex ){ itemsAfterSplice.push( array[i] ); }
}

// Insert all arguments/parameters after numItems
for( i = 3; i < arguments.length; i ++ ){
itemsBeforeSplice.push( arguments[ ''+i ] );
}

// Combine before/after arrays
var remainingItems = itemsBeforeSplice.concat( itemsAfterSplice );

// Rewrite array. Arrays can't be overwritten directly in SSJS
for( i = 0, len=Math.max( array.length, remainingItems.length ); i < len; i++ ){
if( remainingItems.length > i ){
array[i] = remainingItems[i];
} else {
array.pop();
}
}

return splicedItems;
} catch(e){ /*Debug.logException( e );*/ }
}
Note! If you want to do a splice on a scoped field that's an array, you first have to copy it, or your script might crash. E.g.
viewScope.put( 'someArray', [1,2,3,4] ); 
...
var array = viewScope.someArray.concat(); // concat copies the array
$splice( array, 1, 2, 12, 22 ); // modifies the array, returns the removed items [2,3]
viewScope.put( 'someArray', array );// viewScope.someArray -> [1,12,22,4]
I really hope IBM fixes the SSJS API/makes it follow ECMA-262 Edition 3 (or newer editions), but only time will tell.

Tuesday, September 14, 2010

XPages: Workaround for fields losing focus on partial refresh

Update 13.08.13: I found a bug when a link triggered partial refresh. Fixed.

Update 20.03.13: I updated the code so that it doesn't try to focus the field if the partial refresh hasn't influenced the active field.

Update 02.05.11: By request, I made a simple demoapp.

Update 22.11.10: Ajit Rathore found a weakness in the code. On partial refresh error -> unsubscribe to the event. I've added his patch to the code.

The code snippet below, in conjunction with my hijackAndPublishPartialRefresh function should fix the problem of fields losing focus when elements containing them are refreshed.
dojo.addOnLoad(function(){
dojo.subscribe( 'partialrefresh-init', function(){
 // setTimeout needed to make it work in Firefox
 setTimeout(function(){
  var activeElementId = document.activeElement.id;  

  var focusSubscription = dojo.subscribe( 'partialrefresh-complete', function(){
 // Only set focus if field hasn't been overwritten/lost focus
 if( document.activeElement.id !== activeElementId ){
  var activeElement = dojo.byId(activeElementId);

  if( activeElement && /INPUT|SELECT|TEXTAREA/.test( activeElement.nodeName ) ){
   // Set focus to element/select text
   activeElement.focus();
   if( activeElement.nodeName !== 'SELECT' ){
    activeElement.select();
   }
  }
 }
  
   // Unsubscribe after focus attempt is done
   dojo.unsubscribe( focusSubscription );
  });

  // In case of error -> remove subscription
  var errorSubscription = dojo.subscribe( 'partialrefresh-error', function(){
     dojo.unsubscribe( focusSubscription );
  });
 }, 0 );
} );
});
When a partial refresh is initiated, the browser is queried for the active element/the id of the element is stored in a variable. When the refresh is complete, focus is set to the element that had focus before the refresh.

I've only tested it in a small testpage, so there may be bugs that I'm not aware of. Please let me know if you encounter any.

I've tested it/it should work in IE7/8, latest version of Firefox and Opera.

Share and enjoy!

Monday, September 6, 2010

XPages: Helper function for inconsistent API methods - always get an array

Update: I added functionality to convert collections (ArrayList/Vector/etc.) to arrays as well.

// Helper for inconsistent API
// Wrap around @DbLookup/@DbColumn/@Trim/@Unique calls to have an array returned
function $A( object ){
// undefined/null -> empty array
if( typeof object === 'undefined' || object === null ){ return []; }
if( typeof object === 'string' ){ return [ object ]; }

// Collections (Vector/ArrayList/etc) -> convert to Array
if( typeof object.toArray !== 'undefined' ){
return object.toArray();
}

// Array -> return object unharmed
if( object.constructor === Array ){ return object; }

// Return array with object as first item
return [ object ];
}
E.g.
@Unique( 1, 1, 1 ) -> 1
$A( @Unique( 1, 1, 1 ) -> [ 1 ]

@Trim( '', '', 'a', '' ) -> 'a'
$A( @Trim( '', '', 'a', '' ) ) -> ['a']

If IBM fix this inconcistency, your code will not break if you're using the above function. Call it whatever you like. I called it $A from the $A function in MooTools.

Share and enjoy!

Thursday, June 24, 2010

XPages: Simple function to clear scoped variables

All scope objects (applicationScope, sessionScope, etc.) are maps, so it's quite easy to clear them. This might come in handy during development, if you want to clear the applicationScope.

I've tested it on both applicationScope and sessionScope, and it doesn't seem to do any harm. After the maps are cleared, the server automatically loads the "internal" values used by the system.

function clearMap( map:Map ){
// Get iterator for the keys
var iterator = map.keySet().iterator();

// Remove all items
while( iterator.hasNext() ){
map.remove( iterator.next() );
}
}
Usage:
clearMap( applicationScope )

Share and enjoy!

Thursday, June 17, 2010

XPages: Code snippet for Multi Value Custom Converter

I got a question regarding how to work around the buggy multi-value implementation in XPages. I use a custom converter for my multi value fields.

var Converters = {  
multivalue: {
// separator: String or RegExp
getAsObject: function( valuesString, separator ){
try {
separator = separator || ',';
var values = valuesString.split( separator );

// Trims empty values
var trimmedValue, trimmedValues = [];
for( var i = 0; i < values.length; i++ ){
trimmedValue = values[i];
// Removes leading and trailing white-space
if( trimmedValue ){
trimmedValues.push( trimmedValue.replace( /^\s+|\s+$/g, '' ) );
}
}
return trimmedValues;
} catch( e ){ /* Exception handling */ }
},
getAsString: function( values, separator ){
try {
if( values.constructor !== Array ){ values = [ values ]; }
separator = separator || '\n';

return values.join( separator );
} catch( e ){ /* Exception handling */ }
}
}
}
Put the above code snippet inside a script library. Add a custom converter to the multi value field.

In getAsObject -> Converters.multivalue.getAsObject( value, separator );
In getAsString -> Converters.multivalue.getAsString( value, separator );

getAsObject is the conversion of the submitted value to the stored value.
getAsString is the conversion of the stored value to a displayable string value.

Examples:
// Split on comma, semi colon and any white space 
Converters.multivalue.getAsObject( value, /,|;|\s/ );

// Show values comma separated
Converters.multivalue.getAsObject( value, "," );
value is a global variable that's available in the conversion. For getAsObject it's the submitted string. For getAsString, it's the stored value.