The XPage vs. JavaScript library and getElementById issues

Ok so when you create your fields or divs in XPage and you give them a sane Id, when XPage then renders them you get ids like:

id=”view:_id1:_id2:ContentControl:CompanyName”

Please do notice that this applies only to script code which is loaded into the XPage. I’m well aware of that JavaScript entered directly into the Xpage or custom control will not have the problem, since there are many diffrent way to reference the rebuilt XPage-id.

I do see why it is necessary, since you may have multiple forms in one XPage and it would be impossible to keep them apart if fields did not have unique names.

But what if you have your old JavaScript libraries which uses functions like getElementById or if you are smarter than that and uses the dojo.byId() which is browser independent.

There is no way using the client side syntax like #{id:comboBox} in JavaScript libraries. Works fine in XPages and custom controls though.

At the moment I see two approaches, the first is to create a global variable which is then used in the JavaScript library. This method is described here!

The second method is to use dojo.query()!

dojo.query can be used to “query” the DOM tree, a really powerful function which is well worth digging into.

But in this case we will use it to find our html element by querying for the elements where XPage-id ends with the id we are looking for.

So the syntax is dojo.query(“[id$=’:CompanyName’]”).

Let’s take it apart. The brackets means we are searching for a tag with the name “id” ([id$=’…), which value ends with our search string, ‘$=’ stands for ends with.

The search string begins with a colon since we want to be sure we do not get fields with ids like ‘FirstCompanyName’, since they also ends with ‘CompanyName’.

The result will be an array of html elements, with multiple entries, which may be the case!
I’ll try to explain why and when to use which?

The first method is preferred if you have multiple forms as data sources for your fields in the XPage or when you cannot be absolutely sure that the ids are unique over all elements used in your JavaScript library!
In all other cases the second method should work fine!

So now you have to rewrite all your JavaScript libraries! Yes you do! And when you do that try to skip everything which takes an id string as a parameter, rewrite it to use elements or better accepts both types as an argument.

Function doSomthingWithElement(elmnt){
  if(typeof(elmnt) === ‘string’){
    elmnt = dojo.byId(elmnt);
  }
  // use the element
  alert(elmnt.innerHTML);
}

Now with a little twist of customization!

What you also can do is to replace the XSP objects method getElementById with your own implantation of it, using the dojo.query().

Either put the code you find as parameter to dojo.addOnLoad in the client side XPage event onClientLoad or better put the code below in to a JavaScript library where you have your common functions, then add it as a resource into the XPage.

The XSP object is created by Domino server and contains different convenient functions like getElementsById which actually is a wrapper for dojo.byId()!

dojo.addOnLoad(function(){
  dojo.mixin(XSP, {
    getElementById: function(idName){
      var result = dojo.byId(idName);
      if (!result){
        result = dojo.query("[id$=':" + idName + "']");
        if (result){
          result = result[0];
        }
      }
      return result;
    }
  })
});

I’ll try to explain what happens.

First the dojo.addOnLoad() will run the function given as a parameter when the page has loaded in the client. Then by using dojos mixin the script replaces the object literal entry for “getElementById” with the code given as a second parameter. The code itself doing the magic first tries to find the element the classic way by using dojo.byId(). In this way we do not break the functionality. If no elements are found it tries to find the element by using the dojo.query() as described above.

So now if you want to find an element where the id has been replaced by the Domino XPage rendering engine you can use XSP.getElementById(‘CompanyName’).

It’s also possible to add it to the dojo object by altering the code to:

dojo.mixin(dojo, {
    xspById: function(idName){

I’ve did not manage to replace the dojo.byId() since it references it self in a “strange” way which is broken when you use dojo.mixin

If  someone knows of a function which can be used instead in custom JavaScript libraries please comment!

Advertisement

@URLEncode and the JavaScript equivalent!

JavaScript has many different encoding function with different approaches. Some of them does not encode all special characters.
escape() – does not encode: *@-_+./
encodeURI() – Does not encode: ,/?:@&=+$#

But encodeURIComponent() encodes all special characters! Link to W3SChool

@URLEncode(“ISO-8859-1;”,/?:@&=+$#”) will encode to the following string: “%2C%2F%3F%3A%40%26%3D%2B%24%23%22”
Which means that @URLEncode is similar to encodeURIComponent()!

But be warned @URLEncode/@URLDecode in standard uses “Domino” as decode type. “Domino” uses UTF-8 char set which will not work together with standard encoding on a  web page togehter with the JavaScript encode and decode functions . To work well with international characters use “ISO-8859-1”.

When it comes to encoding keep in mind that the decodeURI() function will skip decoding escaped characters which encodeURI() will not encode (at least in Firefox). @URLDecode and the JavaScript functions unescape() and decodeURIComponent() will decode all % escaped characters.

So decoding the string above will give the following result:

unescape() => ,/?:@&=+$#”
decodeURI() => %2C%2F%3F%3A%40%26%3D%2B%24%23″
decodeURIComponent() => ,/?:@&=+$#”

Conclusion:
@URLDecode will decode anything encoded with any of the JavaScript alternatives correctly.
@URLEncode will only be correctly encoded by the JavaScript functions unescape() and decodeURIComponent().

JavaScript in Domino – Unobtrusive JavaScript and mandatory fields

Finally I found a name for it. Chris Blatnick named it on his blog Interface Matters.

The technique was presented to me by Patrick Kwinten a while back. He as also blogged about it here.

Since then I find the technique really appealing since it in my opinion makes the coding in domino a lot cleaner. You can create a javascript library and put all the event management in there instead of having it in forms and subforms. Personally it goes along very well with my belief in separating style and content.

Here’s an example how to dynamically change the behavior on mandatory fields. In this case it uses css to show a red border around the field which is mandatory and is missing a value. I have not included the css here. But it is basic knowledge so it should not be hard to create one your self.

This example requires prototype.js

First a function is used to get a Hash of mandatory fields. In this case the mandatory fields are different depending of a previous selection in a field. The selections are “IBAN”, “US-ABA”, “SE-BG”…
There is a function for this so that the source of the information easily can be changed. Later its easy to implement a configuration document and fetch the information via ajax.

function getMandatoryFields(){

var mandatoryFields = {

"":{"mandatory":["Tx_BankName","Tx_BankID","Tx_AccountNumber
"IBAN":{"mandatory":["Tx_BankName","Tx_BankID","Tx_AccountNumber"]},
"US-ABA":{"mandatory":["Tx_BankName",
"Tx_BankID", "Tx_AccountNumber"]},

"SE-BG":{"mandatory":["Tx_AccountNumber"]},
"SE-PG":{"mandatory":["Tx_AccountNumber"]},
"SE-Clearing":{"mandatory":["Tx_BankName",
"Tx_BankID","Tx_AccountNumber"]},

"DE-BLZ":{"mandatory":["Tx_BankName", "Tx_BankID","Tx_AccountNumber"]},
"CANADA":{"mandatory":["Tx_BankName",
"Tx_BankID", "Tx_AccountNumber"]}};

return mandatoryFields;

}

Here’s the code:
Function refreshMandatoryBankInfo(accountType, mandatoryFields){

//Get the list of fields to the specific account type
mandatoryList = mandatoryFields[accountType].mandatory;

//This is not very modular for now.
var allFields = ["Tx_Recipient","Tx_BankName","Tx_BankID","Tx_AccountNumber"];

//Now we reset all fields. Removing onFocus and onBlur events and styles for mandatory fields
for (var i = 0; i < allFields.length; i++){

$(allFields[i]).setAttribute('onFocus','');
$(allFields[i]).setAttribute('onBlur','');
$(allFields[i]).removeClassName('mandatoryField');
$(allFields[i]).removeClassName('required');

};

//Go through all mandatory fields and assign an onFocus and onBlur event.
for (var i = 0; i < mandatoryList.length; i++){

Event.observe($(mandatoryList[i]), 'focus', mandatoryOnFocusEvent);
Event.observe($(mandatoryList[i]), 'blur', mandatoryOnBlurEvent);

//Set initial class for the field.
if (IsEmpty($(mandatoryList[i]))){

$(mandatoryList[i]).addClassName('mandatoryField');
$(mandatoryList[i]).addClassName('required');

}else{

$(mandatoryList[i]).removeClassName('mandatoryField');
$(mandatoryList[i]).removeClassName('required');

}

}

}

//Function to check if field is empty
function IsEmpty(aTextField) {

if ((aTextField.value.length==0) || (aTextField.value==null)){

return true;

}else {

return false;

}

}

//The onFocus function
function mandatoryOnFocusEvent(e){

//Find source element (field which event was triggered) read more about the technique here. The difference is due to different handling in ie and mozilla.
if (typeof e == undefined) {

var e = window.event;

}
var source;
if (typeof e.target != undefined) {

source = e.target;

} else if (typeof e.srcElement != undefined) {

source = e.srcElement;

} else {

//Since it is a target type we do not handle, exit function
return true;

}

//Set class for active field (You can define a special style to show in which field the cursor is placed).
source.addClassName('activeField');

}

function mandatoryOnBlurEvent(e){

if (typeof e == undefined) {

var e = window.event;

}
var source;
if (typeof e.target != undefined) {

source = e.target;

} else if (typeof e.srcElement != undefined) {

source = e.srcElement;

} else {

return true;

}

//Remove class for active field
source.removeClassName('activeField');

//Different handle for text and textarea and a select object
if (source.type =="text" || source.type =="textarea"){

if (IsEmpty(source)){

source.addClassName('mandatoryField')

} else{

source.removeClassName('mandatoryField')

}
source.value = Trim(source.value);

//Check if select statement is empty. Select-one is for on selection if you have a multiple selection object test for select-multiple. The test if it is empty may then have to be rewritten to work.
}else if (source.type =="select-one"){

if(source.options[source.selectedIndex].text == ""){

source.addClassName('mandatoryField')

}else{

source.removeClassName('mandatoryField')

}

}

}

How the function is called:
refreshMandatoryBankInfo($F("Tx_AccountType"), getMandatoryFields());