Archive | JavaScript RSS for this section

XPages Tip: Beware Server-Side code in Multiple onClientLoad Events

TL;DR – Server-side code in onClientLoad causes a page refresh and prevents additional onClientLoad events with server-side code from running. In this post, I’ll show an example and describe how it behaves.

Page Ready Events

It is extremely useful to have an event trigger that executes when the page is fully loaded; many JavaScript plugins wait for the page to finish loading and then run to activate widgets on the page. Because of its importance, jQuery has $(document).ready() and Dojo has dojo/domReady and dojo.addOnLoad() to make this checking easy.

The Problem with onClientLoad in XPages

XPages provides the onClientLoad event on pages, custom controls, and panels. XPages allows you to run client-side and/or server-side code in the event. However, running server-side code can cause a tough-to-troubleshoot side effect.

Take, for example, this XPage, which has two panels. Both panels and the page itself have client-side and server-side onClientLoad code to write a message to the browser console and server console, respectively.

<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core">

  onClientLoad Event Testing

  <xp:eventHandler event="onClientLoad" submit="true" refreshMode="norefresh">
    <xp:this.script><![CDATA[console.log('Page - onClientLoad');]]></xp:this.script>
    <xp:this.action><![CDATA[#{javascript:print('Page - onClientLoad');}]]></xp:this.action>
  </xp:eventHandler>

  <xp:panel id="panel1">
    <xp:eventHandler event="onClientLoad" submit="true" refreshMode="norefresh">
      <xp:this.script><![CDATA[console.log('Panel 1 - onClientLoad');]]></xp:this.script>
      <xp:this.action><![CDATA[#{javascript:print('Panel 1 - onClientLoad');}]]></xp:this.action>
    </xp:eventHandler>
  </xp:panel>
  
  <xp:panel id="panel2">
    <xp:eventHandler event="onClientLoad" submit="true" refreshMode="norefresh">
      <xp:this.script><![CDATA[console.log('Panel 2 - onClientLoad');]]></xp:this.script>
      <xp:this.action><![CDATA[#{javascript:print('Panel 2 - onClientLoad');}]]></xp:this.action>
    </xp:eventHandler>
  </xp:panel>

</xp:view>

Here’s what happens when the page loads:

  1. The page-level client-side code runs to write the message to the browser console
  2. The page-level server-side code runs to write the message to the server console

That’s it.

Execution on onClientLoad handlers stops at this point because the page has posted to the server because of the server-side code; the onClientLoad event handlers for the panels do not execute at all.

You can look in the browser’s developer tools and see the POST that is sent. It doesn’t try to update anything on the page, so there’s no response, but it’s still enough to interrupt everything else.

More details on the behavior:

  • You can run multiple client-side onClientLoad event scripts without issue — as long as they’re not triggering partial refreshes
  • If the event handlers are rearranged on the page, the one that comes first in the source is the one that will run
  • If there is only one onClientLoad event with server-side code, it will run (based on it’s order in the source), then POST, then let the rest of the onClientLoad events on the page that only have client-side code run. (Any with server-side code will not execute — even the client-side event code on handlers that also have server-side code will not run.)

Mitigating the Problem

This may sound like an easy thing to keep in check, but if you have onClientLoad code on multiple custom controls, it may be hard to make sure that there won’t be a conflict, especially because there won’t be any error thrown.

The strong recommendation is to just not put server-side code in the onClientLoad event, period.

If you absolutely need it, then you may need a standard of putting any necessary onClientLoad code in the common layout control that’s loaded on every page, but it still has the potential to interrupt anything else going on the page or annoy the user by causing a delay immediately after the page is loaded waiting for the post and response. I would try to put code in the afterPageLoad event or use client-side code to trigger an RPC method or custom REST service if server-side code needs to run in that event.

Advertisements

Handling Errors in an XPages RPC Method

The Remote Service (aka xe:jsonRPCService) is an extremely useful control in XPages because it examples client- and server-side code interaction, runs asynchronously, performs well, and is easy to use. But if you don’t handle errors well, it can fail quietly and the user may never know. In this post, I’ll show how to handle client- and server-side JavaScript errors with an RPC method.

Remote Procecure Example

As a starting point, let’s look at an example of a simple RPC method that returns the date and time and displays it for the user.

RPC Control

Here’s the XPages component source:

<xe:jsonRpcService id="jsonRpcService1" serviceName="myRPCService">
  <xe:this.methods>
    <xe:remoteMethod name="myMethod" script="return myLibraryFunction();">
    </xe:remoteMethod>
  </xe:this.methods>
</xe:jsonRpcService>

The service name is myRPCService. The method, myMethod takes no parameters and just calls an SSJS library function named myLibraryFunction().

SSJS Library Function

Here’s the SSJS function that it calls:

function myLibraryFunction() {
  var d = new Date();
  return d.toLocaleString();
}
Client-Side JavaScript

Here’s the code that calls the RPC method:

myRPCService.myMethod().addCallback(function (response) {
  alert('response: ' + response);
})

It calls the method in the RPC service and attaches a callback function to wait for the response. It then displays the response to the user in an alert.

RPC Error Handling - A

Handling Client-Side Errors

This works well and RPC functionality is awesome, but you have to be careful about how you handle errors.

Take for example, this problematic update. This code ignores the response, but tries to display an invalid value (an undeclared variable) in an alert.

myRPCService.myMethod().addCallback(function (response) {
  console.log('before');
  alert ('Invalid variable: ' + myUndeclaredVariable);
  console.log('after');
})

When you run the code, it appears that nothing happens. The console shows the ‘before’ statement, but it then fails quietly.

Putting a try-catch block around it like this doesn’t help at all.

try {
  myRPCService.myMethod().addCallback(function (response) {
    console.log('before');
    alert ('Invalid variable: ' + myUndeclaredVariable);
    console.log('after');
  })
} catch (e) {
	alert('Error: ' + e.toString());
}

This may seem a bit confusing, but it actually makes sense. The callback effectively runs in its own scope, so the error handling needs to be within the callback in order to fire when dealing with an RPC call.

This code does the trick:

myRPCService.myMethod().addCallback(function (response) {
  try {
    console.log('before');
    alert ('Invalid variable: ' + myUndeclaredVariable);
    console.log('after');
  } catch (e) {
    alert('Error: ' + e.toString());
  }
})

RPC Error Handling - B

Handling Server-Side Errors

If there’s an unhandled exception on the server-side code that runs (such as an error or forgetting to return a response), it also fails quietly, although it will show a few errors in the browser console.

RPC Error Handling - C

If you expand the POST, you’ll see that it returns a stack trace for an RPC error.

In this case, error handling in the client-side callback function doesn’t help at all, so it’s important to handle the error in the server-side code and return something that the callback can deal with.

function myLibraryFunction() {
  try {
    print (myUndeclaredVariable);
    var d = new Date();
    return d.toLocaleString();	
  } catch (e) {
    return 'Error: ' + e.toString();
  }
}

RPC Error Handling - D

Now, I’m returning a more informative message to the user about what went wrong.

I generally like to set up my server-side methods to return an object to the callback function, because that way I can add as many properties as I want in order to return all of the information that I need. I generally include a property for a boolean value for whether the method was successful and another value that contains a success or error message. In the callback, those values can be checked and appropriate action can be taken accordingly.

Fixing a Bug with Dropdown Buttons in 9.0.1 FP3 and FP4

If you use Dropdown Buttons in XPages on Domino 9.0.1 and a OneUI 2.x theme, there’s a bug with two recent fixpacks (FP3 and FP4) that breaks them. In this post, I’ll show what happens and share code I’ve used to fix them.

Version Disclaimer

The configuration where I’ve seen this is a test server with Domino 9.0.1 and extension library version 9.0.1.v00_02_20131212-1115. I have not set up other configurations (such as new extension libraries or a non-extension library server) and tested them at this point.

Extension Library Fix

Note: The latest relase of the extension library (Aug 31, 2015) says that it includes a fix for this issue. SPR PEDS9ZKCZU – Fix dropdown button regresssion in OneUI 2.X

Dropdown Button Issue

The dropdown button control should generally look like this when clicked:

DropdownButton_A

However, an issue was introduced with 9.0.1 FP3 (and continued with FP4) that caused it to be generated as a link instead.

DropdownButton_B

The good news is that it still works.

Generated Source

Inspecting the source of the button in both versions, it’s clear to see the difference. The original version generates a button tag.

<div id="view:_id1:dropDownButton1" class="lotusBtnContainer">
  <button aria-owns="view:_id1:dropDownButton1_ab_0_mn" aria-haspopup="true" role="button" id="view:_id1:dropDownButton1_ab_0" class="lotusBtn">
    Update Status 
    <img src="/oneuiv2/images/btnDropDown2.png" aria-label="Show Menu" alt="Show Menu">
    <span class="lotusAltText">
      ▼
    </span>
  </button>
</div>

The version in FP3 and FP4 generates a link.

<div id="view:_id1:dropDownButton2" class="lotusBtnContainer">
  <span>
    <span aria-owns="view:_id1:dropDownButton1_ab_0_mn" aria-haspopup="true" role="button">
      <a id="view:_id1:dropDownButton1_ab_0" href="javascript:;" class="lotusBtn">
        Update Status 
        <img src="/oneuiv2/images/btnDropDown2.png" aria-label="Show Menu" alt="Show Menu">
        <span class="lotusAltText">
          ▼
        </span>
      </a>
    </span>
  </span>
</div>

In both cases, the same JavaScript is generated in a script tag at the end of the page

function view__id1_dropDownButton1_ab_0_mn_ctor(){
var m=new dijit.Menu({"title":"Drop Down Menu"});
var ch0=(new dijit.MenuItem({label:"Draft",onClick:function(){XSP.setSubmitValue("Draft");XSP.fireEvent(arguments[0], "view:_id1:_id4", "view:_id1:dropDownButton1", null, true, 2, null);}}));
m.addChild(ch0);
var ch1=(new dijit.MenuItem({label:"Submitted",onClick:function(){XSP.setSubmitValue("Submitted");XSP.fireEvent(arguments[0], "view:_id1:_id4", "view:_id1:dropDownButton1", null, true, 2, null);}}));
m.addChild(ch1);
var ch2=(new dijit.MenuItem({label:"Approved",onClick:function(){XSP.setSubmitValue("Approved");XSP.fireEvent(arguments[0], "view:_id1:_id4", "view:_id1:dropDownButton1", null, true, 2, null);}}));
m.addChild(ch2);
var ch3=(new dijit.MenuItem({label:"Rejected",onClick:function(){XSP.setSubmitValue("Rejected");XSP.fireEvent(arguments[0], "view:_id1:_id4", "view:_id1:dropDownButton1", null, true, 2, null);}}));
m.addChild(ch3);
var ch4=(new dijit.MenuItem({label:"Cancelled",onClick:function(){XSP.setSubmitValue("Cancelled");XSP.fireEvent(arguments[0], "view:_id1:_id4", "view:_id1:dropDownButton1", null, true, 2, null);}}));
m.addChild(ch4);
return m;
}

The Fix

To fix it, I wrote a JavaScript function that takes the generated output and changes it back to the original output. It also re-attaches the event handling functions to each menu option. I added the function to a client-side JavaScript library on the page and then called it with the client-side ID of a drop-down button control to fix it.

Note: Testing has been limited, but there are no known issues at this time. Please test thorougly before putting into a production environment.

Here’s the library function:

function fixDropDownButtons(clientID) {
  try {
  
  var dropdownDiv = dojo.byId(clientID);
  var html = dropdownDiv.innerHTML;
  
  // Remove opening and both closing span tags
  html = html.substring(13, html.length-16);
  
  // Get the attributes from the next span tag (parse from after <span to the next left angle bracket -1
  var idx = html.indexOf('>');
  var attributes = html.substr(0, idx);

  // Add the attributes to the next tag
  html = html.substr(idx + 4);
  html = '<button ' + attributes + ' ' + html;

  // Remove the href="javascript:;" attribute
  idx = html.indexOf('href=');
  
  // NOTE: in IE11, the href="javascript:;" attribute is the last attribute, so it goes up to the '>'
  // In Firefox, it's not the last attribute, so it can be located by the next space
  // Solution: Find the next space and the next closing bracket and use the lower of the two numbers
  var idxSpace = html.indexOf(' ', idx+1) + 1;
  var idxBracket = html.indexOf('>', idx+1);
  var idx2 = idxSpace < idxBracket ? idxSpace : idxBracket;
  html = html.substr(0, idx) + html.substr(idx2);
  
  html = html.replace('</a>', '</button>');
  console.info(html);
  
  dropdownDiv.innerHTML = html;
  
  // Re-connect the event handler that the server already generated.
  // This assumes that it's part of the global window object
  var clientID_noColon = clientID.replace(/:/g, '_');
  dojo.connect(dojo.byId(clientID + "_ab_0"),"onclick",function(thisEvent) {
    XSP.openMenu(thisEvent,eval(clientID_noColon + "_ab_0_mn_ctor"));
  });
  } catch (e) {
    console.warn('Error in fixDropDownButtons(' + clientID + ') - ' + e.toString());    
  }

}

It’s largely text parsing and rearranging once it gets the HTML generated for the component. (Read code comments for more details.)

Toward the end, it re-connects event handlers for the dropdown button options. Fortunately, there’s a predicatable naming pattern, so it can consistently determine how to find the correct functions to re-attach.

The code also accounts for the fact that the HTML attributes are generated in differnet ordres in different browsers, which affects the text parsing.

Calling the Function

Here’s how to call it from the onClientLoad event of the page:

fixDropDownButtons("#{id:dropDownButton1}");

This code uses pass-thru logic to determine the button’s client-side JavaScript ID and passes it to the function.

Potential Improvements

This is my initial solution, but there are a few ways that it could be quickly enhanced.

First of all, it could be enhanced to work based on a class or just automatically finding drop-down buttons rather than having to determine the client-side ID and pass it to the function.

It could also be updated to only fix buttons as needed, in order to prevent it from causing problems when you update to a fixpack or extension library version that solves the original bug. A simple check for whether it has a button tag could determine this.

Initializing Global Variables and Synchronous AJAX Calls in an Angular XPages Application

In a recent project (an XPages application with an Angular front end), I had a need to (a) run a script to determine user access and (b) store that value globally. In this post, I’ll show one way to achieve this with a synchronous AJAX call and a module run block.

Use Case

In this application, the front end pages need to know whether the user has Admin rights in the application in order to determine whether to display advanced options such as configuration links.

The security check needs to be run before the page is loaded and it needs to be stored globally so that the security check does not have to run constantly as many page elements are loaded.

Initial Implementation

I created a custom REST service that checks the user’s roles and returns a boolean value for whether the user is an Admin in the application. I called it via AJAX and stored the value in the $rootScope.

I then set up the logic from each admin-controlled feature call a function to check whether the value existed in $rootScope. If not, the function would make the AJAX call to look up the value and store it.

The code to call the REST service looks like this:

$http.get('rest.xsp/isUserAdmin').success(function (data) {
  $rootScope.isAdmin = (data == 'true') ? true : false;
})

The logic seemed to be set up properly. However, it didn’t initially work as I expected.

The Problem

Watching the Net panel in the browser tools made it clear that the AJAX call was being made many times as each page loaded.

The AJAX calls were fired off so rapidly that numerous calls were made before any of them had a return value available in the $rootScope.

It turns out that Angular’s AJAX calls are asynchronous by design. For the most part, this is great because it keeps the application more responsive. However, in this case, I needed the security check to complete before loading the rest of the page.

The Solution

Ultimately, I solved the problem by making two changes.

  1. Moved the role-checking code to a module run block so it would run before page load
  2. Replaced the Angular AJAX call with a standard jQuery AJAX call so I could add a parameter to force the call to be synchronous.

As the application loads, it will call the REST service and wait for the response. When done, it will store the value in the $rootScope.

Here is a link to the Angular.js module documentation.

Run blocks are the closest thing in Angular to the main method. A run block is the code which needs to run to kickstart the application. It is executed after all of the service have been configured and the injector has been created.

A run block generally goes in the app.js file rather than in a controller. Just add a run() method to the application and include the required directive and code.

This code sets up a function in the run block that includes a synchronous jQuery AJAX call to an XPages REST service, and stores the return value in the $rootScope for global access in the application. This runs immediately and the global variable is initialized and available when the rest of the page loads.

// Make the user access check available globally
PSApps.run(['$rootScope',                    
  function($rootScope) {
	 
      // Use jQuery for a synchronous http request -- angular hard-codes all requests to be asynchronous
      $.ajax('rest.xsp/isUserAdmin', {async: false})
        .success(function (data) {
          $rootScope.isAdmin = (data == 'true') ? true : false;
          console.log('is user admin? ' + data);
      });

  }
]);

Then, all places that need to check the value in order to determine whether to display an element can just reference the global property.

ng-show="$rootScope.isAdmin"

Client-Side JavaScript Debugging Tip — Using XSP.dumpObject() and XSP.log()

The XSP object is a client-side JavaScript object that is built and delivered by the XPages runtime and it contains a number of useful functions, some of which are always available and some of which are context-specific. In this post, I’ll show two functions that are useful for client-side JavaScript debugging.

XSP.dumpObject()

The dumpObject() method returns a string representation of all properties of any object that you pass to it. If you want to track down why something isn’t working as expected, it may be helpful to inspect the properties of the object.

The object can be a variable that you programmatically create and modify or it can be an element on the page.

I used to make frequent use of a small script to display all properties of a JavaScript object, but this is a much simpler way to get that information.

Here’s an example of client-side JavaScript to build an object and use XSP.dumpObject() to write out all properties to the browser console:

var myObject = {}
myObject.property1 = 'One';
myObject.property2 = 'Two';
myObject.property3 = 'Three';
myObject.arrayProperty = ['a', 'b', 'c'];

console.log(XSP.dumpObject(myObject));

This information can be very helpful in debugging, so I would recommend using it when handling errors in a try-catch block.

XSP.log()

In addition to logging to the console, you have another similar option available by another XSP function. XSP.log() is analogous to the standard console.log() function that you use when debugging client-side

JavaScript, but the log() method opens up a new browser window and logs messages there, in descending order.

All we have to do is change the last line in the previous snippet:

XSP.log(XSP.dumpObject(myObject));

And this is what we see in a separate browser window:

XSPLog - 1

The log() method will continue appending any messages that you send until you close the window. The next call to XSP.log() will open a new, blank window and start logging again.

Using dumpObject on an HTML field and XPages control

You can also use dumpObject on an HTML element.

For example, if I have a pure HTML select with 3 options and an onchange event, this will be the output of if I get a handle to the element (the id is HTMLComboBox) and dump the object properties:

XSP.log(XSP.dumpObject(dojo.byId('htmlComboBox')));

object
  0: [object HTMLOptionElement] (Maximum Depth Reached)
  1: [object HTMLOptionElement] (Maximum Depth Reached)
  2: [object HTMLOptionElement] (Maximum Depth Reached)
  autofocus: false
  disabled: false
  form: [object HTMLFormElement] (Maximum Depth Reached)
  multiple: false
  name: 
  required: false
  size: 0
  type: select-one
  options: [object HTMLOptionsCollection] (Maximum Depth Reached)
  length: 3
  selectedOptions: [object HTMLCollection] (Maximum Depth Reached)
  selectedIndex: 0
  value: v1
  willValidate: true
  validity: [object ValidityState] (Maximum Depth Reached)
  validationMessage: 
  title: 
  lang: 
  dir: 
  dataset: [object DOMStringMap] (Maximum Depth Reached)
  itemScope: false
  itemType:  (Maximum Depth Reached)
  itemId: 
  itemRef:  (Maximum Depth Reached)
  itemProp:  (Maximum Depth Reached)
  properties: [object HTMLPropertiesCollection]... (Maximum Depth Reached)
  itemValue:  (Maximum Depth Reached)
  hidden: false
  tabIndex: 0
  accessKey: 
  accessKeyLabel: 
  draggable: false
  contentEditable: inherit
  isContentEditable: false
  contextMenu:  (Maximum Depth Reached)
  spellcheck: false
  style: [object CSS2Properties] (Maximum Depth Reached)
  className: 
  oncopy:  (Maximum Depth Reached)
  oncut:  (Maximum Depth Reached)
  onpaste:  (Maximum Depth Reached)
  offsetParent: [object HTMLBodyElement] (Maximum Depth Reached)
  offsetTop: 39
  offsetLeft: 0
  offsetWidth: 67
  offsetHeight: 19
  onabort:  (Maximum Depth Reached)
  onblur:  (Maximum Depth Reached)
  onfocus:  (Maximum Depth Reached)
  oncanplay:  (Maximum Depth Reached)
  oncanplaythrough:  (Maximum Depth Reached)
  onchange:  (Maximum Depth Reached)
  onclick:  (Maximum Depth Reached)
  oncontextmenu:  (Maximum Depth Reached)
  ondblclick:  (Maximum Depth Reached)
  ondrag:  (Maximum Depth Reached)
  ondragend:  (Maximum Depth Reached)
  ondragenter:  (Maximum Depth Reached)
  ondragleave:  (Maximum Depth Reached)
  ondragover:  (Maximum Depth Reached)
  ondragstart:  (Maximum Depth Reached)
  ondrop:  (Maximum Depth Reached)
  ondurationchange:  (Maximum Depth Reached)
  onemptied:  (Maximum Depth Reached)
  onended:  (Maximum Depth Reached)
  oninput:  (Maximum Depth Reached)
  oninvalid:  (Maximum Depth Reached)
  onkeydown:  (Maximum Depth Reached)
  onkeypress:  (Maximum Depth Reached)
  onkeyup:  (Maximum Depth Reached)
  onload:  (Maximum Depth Reached)
  onloadeddata:  (Maximum Depth Reached)
  onloadedmetadata:  (Maximum Depth Reached)
  onloadstart:  (Maximum Depth Reached)
  onmousedown:  (Maximum Depth Reached)
  onmouseenter:  (Maximum Depth Reached)
  onmouseleave:  (Maximum Depth Reached)
  onmousemove:  (Maximum Depth Reached)
  onmouseout:  (Maximum Depth Reached)
  onmouseover:  (Maximum Depth Reached)
  onmouseup:  (Maximum Depth Reached)
  onpause:  (Maximum Depth Reached)
  onplay:  (Maximum Depth Reached)
  onplaying:  (Maximum Depth Reached)
  onprogress:  (Maximum Depth Reached)
  onratechange:  (Maximum Depth Reached)
  onreset:  (Maximum Depth Reached)
  onscroll:  (Maximum Depth Reached)
  onseeked:  (Maximum Depth Reached)
  onseeking:  (Maximum Depth Reached)
  onselect:  (Maximum Depth Reached)
  onshow:  (Maximum Depth Reached)
  onstalled:  (Maximum Depth Reached)
  onsubmit:  (Maximum Depth Reached)
  onsuspend:  (Maximum Depth Reached)
  ontimeupdate:  (Maximum Depth Reached)
  onvolumechange:  (Maximum Depth Reached)
  onwaiting:  (Maximum Depth Reached)
  onmozfullscreenchange:  (Maximum Depth Reached)
  onmozfullscreenerror:  (Maximum Depth Reached)
  onmozpointerlockchange:  (Maximum Depth Reached)
  onmozpointerlockerror:  (Maximum Depth Reached)
  onerror:  (Maximum Depth Reached)
  tagName: SELECT
  id: htmlComboBox
  classList:  (Maximum Depth Reached)
  attributes: [object MozNamedAttrMap] (Maximum Depth Reached)
  onwheel:  (Maximum Depth Reached)
  scrollTop: 0
  scrollLeft: 0
  scrollWidth: 67
  scrollHeight: 19
  clientTop: 0
  clientLeft: 0
  clientWidth: 67
  clientHeight: 19
  scrollTopMax: 0
  scrollLeftMax: 0
  innerHTML: <option value="v1">Value 1</option><option value="v2">Value 2</option><option value="v3">Value 3</option>
  outerHTML: <select id="htmlComboBox"><option value="v1">Value 1</option><option value="v2">Value 2</option><option value="v3">Value 3</option></select>
  previousElementSibling: [object HTMLBRElement] (Maximum Depth Reached)
  nextElementSibling: [object HTMLBRElement] (Maximum Depth Reached)
  children: [object HTMLCollection] (Maximum Depth Reached)
  firstElementChild: [object HTMLOptionElement] (Maximum Depth Reached)
  lastElementChild: [object HTMLOptionElement] (Maximum Depth Reached)
  childElementCount: 3
  nodeType: 1
  nodeName: SELECT
  baseURI: http://127.0.0.2/BlogTesting.nsf/dumpObject.xsp
  ownerDocument: [object HTMLDocument] (Maximum Depth Reached)
  parentNode: [object HTMLFormElement] (Maximum Depth Reached)
  parentElement: [object HTMLFormElement] (Maximum Depth Reached)
  childNodes: [object NodeList] (Maximum Depth Reached)
  firstChild: [object HTMLOptionElement] (Maximum Depth Reached)
  lastChild: [object HTMLOptionElement] (Maximum Depth Reached)
  previousSibling: [object Text] (Maximum Depth Reached)
  nextSibling: [object HTMLBRElement] (Maximum Depth Reached)
  nodeValue:  (Maximum Depth Reached)
  textContent: Value 1Value 2Value 3
  namespaceURI: http://www.w3.org/1999/xhtml
  prefix:  (Maximum Depth Reached)
  localName: select
  ELEMENT_NODE: 1
  ATTRIBUTE_NODE: 2
  TEXT_NODE: 3
  CDATA_SECTION_NODE: 4
  ENTITY_REFERENCE_NODE: 5
  ENTITY_NODE: 6
  PROCESSING_INSTRUCTION_NODE: 7
  COMMENT_NODE: 8
  DOCUMENT_NODE: 9
  DOCUMENT_TYPE_NODE: 10
  DOCUMENT_FRAGMENT_NODE: 11
  NOTATION_NODE: 12
  DOCUMENT_POSITION_DISCONNECTED: 1
  DOCUMENT_POSITION_PRECEDING: 2
  DOCUMENT_POSITION_FOLLOWING: 4
  DOCUMENT_POSITION_CONTAINS: 8
  DOCUMENT_POSITION_CONTAINED_BY: 16
  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC: 32

If I have a similar Combo Box control in XPages, the output is the same because the HTML element that is generated is virtually identical.

Note: You must pass a fully-qualified client-side ID to dumpObject(), so if you’re passing it an XPages element, use the pass-thru EL syntax "#{id:myControl}" or a client-side query that looks for the element id ending with the name of the control, such as dojo.query('[id~=myControl]')[0].id

console.dir(object)

There’s a method built into browser development tools that performs a similar function. This can be similarly handy when troubleshooting something directly on the page.

For example, to see all of the properties of the first input field on the form, you can use it like this:

console.dir(dojo.query('input')[0]);

Order of execution for client-side JavaScript event handlers and callbacks

There are a number of different places from which you can execute client-side JavaScript upon clicking an xp:button. In this post, I’ll document both the order of execution and which ones execute for each refreshMode option.

Client-Side Javascript Events and Callbacks

There are 5 ways that I can see to execute client-side JavaScript when a button is clicked.

The easiest way is the “Client” event tab. Use the script editor to add code.

There are also 3 callbacks on the event handler (onStart, onComplete, onError) where you can add client-side JavaScript. These are a little harder to get to — you have to switch to the Source view, click on the xp:eventHandler tag, and then find them in All Properties under the events category.

EventCallbacks

You can also run client-side JavaScript from an SSJS code using the view.postScript() method. (Here’s an example snippet from Russ Maher) This is a bit more complex because you need to get code into a string (watch the quotes closely!), but it works well.

Here’s an example:

view.postScript('alert("server event - view.postScript");');

An Example

Here’s an example of a button that makes use of all 5 options. xp:this.script is the client-side event. xp:this.action is the SSJS event code with the view.postScript call. onComplete, onError, and onStart are part of the xp:eventHandler itself.

<xp:button value="Refresh Fields 2-4" id="button2">
  <xp:eventHandler event="onclick" submit="true"
    onComplete="alert('server event callback - oncomplete');"
    onError="alert('server event callback - onerror');"
    onStart="alert('server event callback - onstart');"
    refreshMode="partial" refreshId="field1">

    <xp:this.script>
      <![CDATA[alert('client event');]]>
    </xp:this.script>

    <xp:this.action>
      <![CDATA[#{javascript:view.postScript('alert("server event - view.postScript");');}]]>
    </xp:this.action>
  </xp:eventHandler>
</xp:button>

Order of Execution

The callbacks that run very based on the refreshMode property of the event.

If the page is fully refreshed, then none of the event handler callbacks are triggered. If the event is set to No Update or No Submission, then view.postScript will not run.

Here is the order of execution for each event and callback (with the exception of onError, because it would break the flow) that runs, based on the refreshMode:

  • Full Refresh — client event, view.postScript
  • Partial — client event, onstart, view.postscript, oncomplete
  • No Update — client event, onstart, oncomplete
  • No Submission — client event

The only way they all run is with a partial refresh.

Generated Code in the Browser

Here’s what gets generated and passed to the browser for our sample button (when set to partial refresh). The client-event code is defined in its own function. In the XSP.attachPartial() call, the client side event handler function and the callback functions are all passed (along with the button ID, refresh target ID, etc)

<script type="text/javascript">

function view__id1__id13_clientSide_onclick(thisEvent) {
alert('client event');
}

XSP.addOnLoad(function() {
XSP.attachPartial("view:_id1:_id13", "view:_id1:button2", null, "onclick", view__id1__id13_clientSide_onclick, 2, "view:_id1:field11", "alert(\'server event callback - 

onstart\');", "alert(\'server event callback - oncomplete\')", "alert(\'server event callback - onerror\')");
});

</script>

The Client-Side Javascript Event vs the onStart Callback

Of particular interest to me is the distinction between the onStart callback of a server side event and the client-side JavaScript event.

I like the concept of the onStart callback, because your code can be consistent if you’re also running code onComplete, however, the client-side JS event is easier to get to and it also has the ability to return false and cancel execution of the server-side event. (The onStart callback throws an error if you try to return false, because it’s not a function.)

If you have any other insight into the differences or a benefit to using the onStart callback of the event handler, I’d love to hear it.

JavaScript Tip: Checking Multiple Cases in a Switch Statement

I recently worked on a JavaScript function that used a switch statement to branch the logic based on a certain value, but each action applied to multiple different cases. It would be overkill to repeat the same logic in multiple case statements. Fortunately, JavaScript can handle this, although the structure of the code might not be what you’d expect.

My instinct is to try something like this:

switch (myVariable)
{
  case "A", "B", "C":
    // Do something
    break;
  case "D", "E":
    // Do something else
    break;
  default: 
    // Default case
    break;
}

But that doesn’t work.

The correct syntax makes use of the ‘fall-through’ behavior of the JavaScript switch statement. Once it matches a case, it will keep executing code until it either hits a break statement or the end of the switch.

This code will do the trick:

switch (myVariable)
{
  case "A":
  case "B":
  case "C": 
    // Do something
    break;
  case "D":
  case "E":
    // Do something else
    break;
  default: 
    // Default case
    break;
}

In this example, if the value of myVariable is A, B, or C, it will execute the code under case "C":.