LabVantage JavaScript Guide |
Content |
||||||||||||||||||
|
Introduction |
|
|
The LabVantage JavaScript Global API provides developers with enhanced client-side functionality. This document outlines standards for JavaScript authoring, details performance considerations, and describes LabVantage JavaScript Global API functionality.
Writing JavaScript |
|
|
General Guidelines |
Follow these guidelines when writing JavaScript for LabVantage:
|
Script Files Try to use script files rather than writing the script within the JSP or HTML. See Script Files for details. |
|||||||||||
|
Naming Conventions Use these JavaScript naming conventions (refer to http://articles.techrepublic.com.com/5100-22-5109485.html):
|
|||||||||||
|
Use JavaScript Object Orientation Write your new scripts within JavaScript objects. See Objects & Classes for details. |
|||||||||||
|
Keep Quotes Consistent Generally, JavaScript code is written using single quotes (in contrast with double quotes for HTML and Java). The benefit with using single quotes over double quotes is that you can type JavaScript into Java code and into HTML tags without having to escape every double quote. Whichever quote type you use, make sure it is consistent for your entire script file. This will minimize confusion and bugs. |
|||||||||||
|
Do Not Over Comment It is good practice to comment areas of code to help other developers (and you) identify what the variable and algorithm is doing. However, you should limit your comments to an absolute minimum. It has been observed that developers often place JavaDoc-type descriptions at the beginning of methods, properties and JS files. The problem with this is it significantly increases the bandwidth required to download the script file, and slows the interpreter speed as it processes the larger file. If you have written a complicated file that requires detailed comments, consider writing a simple API document that describes the script, then pointing to this in your JS file. |
|||||||||||
|
Be Strict With Type JavaScript is a type-less language, and variables can be converted from one type to the other without any casting. However, using one variable for multiple types can cause confusion and introduce bugs. Therefore, make sure you declare a variable that is used for just one type. If you then convert the type of the variable, create a new variable to hold this new type. You should use the Hungarian Notation naming convention outlined above to assist in the type management of your code. |
|||||||||||
|
Write For Performance JavaScript is an interpreted language and unlike Java (which uses a complier), it is not forgiving of poorly written code. Therefore, you should obey the performance tips that are in the Performance section of this document. |
|||||||||||
|
Parentheses Take advantage of parentheses to ensure that a mathematical result is calculated as expected. For example, are x and y equal: var x = 2 * 3 + 2 / 2 + 2 var y = (((2 * 3) + 2)/(2 + 2)) |
|||||||||||
|
Coding style Use a consistent style when writing JavaScript. This makes it easier to understand from other developers. The coding style to use is the same as Java code, with curly brackets at the end of the line, and a space after the parentheses: function MyClass( ){ this.myFunction = new function( sMyString, iMyInt ){ for ( var iIndex = 0; iIndex < 10; iIndex++ ){ alert( iIndex ); } } } |
|||||||||||
|
Semicolons JavaScript can be written with or without semicolons at the end of a statement. To be consistent throughout LabVantage, all new JavaScript code should be written with a semicolon at the end of a line or statement. |
Script Files |
JavaScript in LabVantage has evolved from a few lines of code that changes the color of a button, to thousands of lines of code that control all aspects of the application. JavaScript development habits must evolve with the language.
For example, placing code directly into a JSP or HTML page rather than into a separate script file causes a big performance drain, as this code has to be downloaded to the client every time the page loads.
Therefore, new code (or any code encountered during a JSP change) should be placed into a related JS file. This JS file should then be placed in the "scripts" folder for the context of your item. For example, if you write a new Page Type called "SuperPageType" with a JS class of SuperPageType, all JavaScript code for this page type should be placed in a "superpagetype.js" (same name but in lower case) file in the "scripts" folder associated with this Page Type. Alternatively, if you write an Element called "MyElement", create a script file called "myelement.js", and place it in the scripts folder for this Element. Here are some examples:
Item | Item Name | JS Class Name | JS Object Name | JS Filename | JS File Location |
Element | ANewElement | ANewElement | aNewElement | anewelement.js | WEB-CORE/elements/scripts/ |
Page Type | PowerList | PowerList | powerList | powerlist.js | WEB-CORE/pagetypes/scripts/ |
Page | SampleList | SampleList | sampleList | samplelist.js | WEB-CORE/pagetypes/list/scripts/ |
Utility in Module | LV_BioBankList
LV_AnotherBBList | N/A | N/A | N/A | Submit for approval as a new Module. |
The LabVantage API |
Begining with R4.7, LabVantage contains a comprehensive and documented JavaScript Global API for public use. Not only should your new functions and objects use this API wherever possible, but if you write a global feature in JavaScript, the feature should be placed into this API.
Objects & Classes |
When writing JavaScript, use object orientation. This means you create a JS file with several global functions such as "addAThing" and "removeAThing", then include this into your page. However, if another developer also creates a function called "addAThing" and includes it into the same page, the two objects will override each other. Imagine a LabVantage Maintenance page with two different Elements, each created by a different developer.
As you will see with the new API files, we now employ a package-like structure to our client-side script. This has the benefit of not only separating functionality, but also protects against such clashes.
Therefore, when you create a new script file for an Element, Page Type, or feature, you must first create a new class for this item using the same name. For example, if you create a new Element called "MyElement", you will have a JS file called "myelement" with a class called "MyElement" (see Script Files for examples).
To create a class in JavaScript, use the function syntax:
function MyElement(){
// this is a function as a class
}
Then, to instantiate the object, use this syntax:
var myelement = new MyElement();
In any script files you create you should make the object self instantiating by placing the initialization script at the end of the file:
function MyElement(){
// this is a function as a class
}
if (typeof(myelement) == 'undefined'){
var myelement = new MyElement();
}
Therefore, any class can be called as soon as the file is included into the HTML or JSP. The "typeof()" line makes sure that that object does not already exist in the current scope. To add a public property (accessible from outside the object using obectname.property) to the class, use the "this.propertyName" syntax:
function MyElement(){
this.myPublicProperty = 'Hello World';
}
To add a private property (accessible only from functions within the object) to the object, use the "var propertyName" syntax:
function MyElement(){
var myPrivateProperty = 'Hello World Again';
}
To declare a public function within the object, use the syntax:
function MyElement(){
this.myPublicFunction = function(){
// function contents
}
}
To declare a private function within the object, use the syntax:
function MyElement(){
function myPrivateFunction(){
// function contents
}
}
Example |
This is an example file called "MyElement.js". It is for a new Element called "MyElement". The file should be placed in "WEB-CORE/elements/scripts/".
function MyElement(){
this.sMyName = 'Steve';
this.sMyEyeColor = 'blue';
var iMyAge = 28;
this.setUser = function( sName, sEyeColor, iAge ){
this.sMyName = sName;
this.sMyEyeColor = 'blue';
iMyAge = iAge;
sayHello( );
}
this.getAge = function( ){
return iMyAge;
}
function sayHello( sName, iAge ){
alert( 'Hello ' + sName + '. You are ' + iAge + '.' );
}
}
if ( typeof(myelement) == 'undefined' ){
var myelement = new MyElement( );
}
The JSP that is part of my element can then call the functions from the file once it has been included.
<script src="WEB-CORE/elements/scripts/MyElement.js"></script>
<script>
myelement.setUser( 'Steve', 'Blue', 28 );
alert( myelement.sMyName );
alert( myelement.sMyEyeColor );
alert( myelement.getAge( ) );
</script>
Useful API Functions |
As mentioned above, the JavaScript API provides you with a global set of objects and functions that can do anything from show a dialog to perform an Ajax request. The API is accessible from any page or iframe that uses one of the LabVantage Layouts. Although the LabVantage JavaScript Global API document details every object, property and method, here are some of the methods you should be using rather than coding them yourself:
| Dialogs The sapphire.ui.dialog object controls and shows new LabVantage dialogs and popups. This shows an alert box... use it instead of "alert()": sapphire.ui.dialog.alert( 'hello world' ) This shows a more detailed alert box... use it instead of "alert()", "showModlessDialog" or "showModalDialog": sapphire.ui.dialog.show( 'My Message', 'Hello' ) This prompts for text entry... use it instead of "prompt()": sapphire.ui.dialog.prompt( 'My Prompt', 'Please enter' ) This confirms a decision for you... use it instead of "confirm()": sapphire.ui.dialog.confirm( 'My Confirm', 'Please confirm' ) | |
| Ajax The sapphire.ajax object controls the new Ajax API. This calls a LabVantage Ajax class with the sent properties: sapphire.ajax.callClass( 'com.labvantage.sapphire.myAjaxClass', myCallbackFunction, myProps ) This calls a LabVantage Ajax service with the sent properties: sapphire.ajax.callService( 'com.labvantage.sapphire.myAjaxService', myCallbackFunction, myProps ) This calls a LabVantage Ajax command with the sent properties: sapphire.ajax. callCommand( 'com.labvantage.sapphire.myAjaxCommand', myCallbackFunction, myProps ) | |
| Connection Information Using the sapphire.connection object and sapphire.page object, you can get connection details, user information, and page information. To get the connection Id of the LabVantage connection: sapphire.connection.connectionId To get the database Id of the LabVantage connection: sapphire.connection.databaseId To get the user Id of the current LabVantage user: sapphire.connection.sysUserId To get the current page name: sapphire.page.name | |
| Maintenance List and Maintenance Form You can use the sapphire.page.list and sapphire.page.maint objects to perform public functionality on List pages and Maintenance pages, respectively. Here are a few examples: To open the Add Maintenance page: sapphire.page.list.add( sAPage, sTarget ) To get the selected items in the current page or all pages (and return all default columns or the specified columns): sapphire.page.getSelected( '', true ) To open the Add Maintenance page: sapphire.page.maint.add(sAPage, sTarget ) To obtain keyid1 from the Maintenance page: sapphire.page.maint.getKeyId1 ( ) | |
| Animations R4.6 introduced graphical animations on the user interface. These animations can be switched on and off for each user, or globally for the application. You can use these animations to show and hide any HTML object. To show an object using a fade in transition: sapphire.ui.animation.fadeIn( 'myDiv', 10, 20, 0, 100 ) To hide an object using a fade out transition: sapphire.ui.animation.fadeOut( 'myDiv', 10, 20, 0, 100 ) To show an object using a resize in vertical transition: sapphire.ui.animation.showObjectVertical ( 'myDiv', 10, 20 ) To show an object using a resize in horizontal transition: sapphire.ui.animation.showObjectHorizontal( 'myDiv', 10, 20 ) To hide an object using a resize out vertical transition: sapphire.ui.animation.hideObjectVertical( 'myDiv', 10, 20 ) To hide an object using a resize out horizontal transition: sapphire.ui.animation.hideObjectHorizontal ( 'myDiv', 10, 20 ) | |
| Drag & Drop and Lines The API also contains full management for drag & drop, and drawing dynamic lines/links between HTML elements (such as flow charts). See the LabVantage JavaScript Global API document for more information on sapphire.ui.dragdrop and sapphire.ui.link. |
Performance |
| |
Performance is a major factor to consider as client-side functionality grows. JavaScript (unlike Java) is an interpreted language, and calls its methods and properties through a form of structured referencing. Therefore, code that would normally perform well in a compiled language can run very slowly in JavaScript. To write more efficient and faster code, these guidelines should be followed:
| Cache your scripts As mentioned earlier, if you type code directly into an HTML page or JSP page then that script content is downloaded to the client every time the page loads or refreshes. For large script content this can be a huge performance overhead. Therefore, unless absolutely necessary, all script should be placed in an external JS file and not in the JSP itself. You should also try and restrict all your JavaScript for one page to one file rather than multiple files. This is due to the fact that when using multiple JS files a HTTP connection to the server is required for each file, which can reduce performance. | |
| Cache your objects The nature of JavaScript is to always lookup a reference to an object or property and therefore each time you call "document.all.length" the JavaScript engine will lookup the "document" object and then the "all" object before obtaining the "length" property. If this is called repeatedly in a loop or a number of times in a method then noticeable performance degradation can occur. Therefore by storing the object in a variable and then using the variable we get better performance: Bad example: span style='font-size:9.0pt; font-family:"Courier New"'>alert('The length is: ' + document.all.length); while ( true ){ if ( document.all ){ alert(document.all[0]); } } Good example: var oDocAll = document.all; alert("The length is: " + oDocAll.length); while ( true ){ if ( oDocAll ){ alert(oDocAll[0]); } } | |
| Cache your properties As with objects, JavaScript will lookup a property using a reference mechanism every time it is accessed. Therefore, it is also important to cache your properties, especially when used successively in iterations (such as a for loop. Bad example: var sMyString = ''; for ( var iIndex = 0; iIndex < saLongArray.length; iIndex ++ ){ sMyString += saLongArray[iIndex]; } Good example: var sMyString = ''; var iLength = saLongArray.length; for ( var iIndex = 0; iIndex < iLength; iIndex ++ ){ sMyString += saLongArray[iIndex] } | |
| Use "var" Always declare the function level variables with the "var" keyword. If you declare a new variable in JavaScript without the "var" keyword then the variable is given a global scope. This not only could lead to clashes in the code from other variables with the same name, but also will keep the variable or object active in memory until the page is disposed. Therefore you must always declare local function variables using the "var" keyword. Bad example: function myFunction( ){ sMyString = ''; sMyString += 'Hello'; alert( sMyString ); } Good example: function myFunction( ){ var sMyString = ''; sMyString += 'Hello'; alert( sMyString ); } | |
| Fragment documents Use document fragments (rather than using the document object directly) when updating the DOM. When you update the document structure using "createElement" and "appendChild", the document refreshes its structure at every update. This can be a large performance drain. To prevent this, you should use the "document.createDocumentFragment" method to return a workable fragment of the document that can then be appended to the parent document after you are done with it. Bad example: var oEl1 = document.createElement('div'); oEl1.id = 'myDiv'; var oEl2 = document.createElement('span'); oEl2.id = 'mySpan'; var oEl3 = document.createElement('font'); oEl3.innerText = 'Hello'; document.body.appendChild(oEl1); document.body.appendChild(oEl2); document.body.appendChild(oEl3); Good example: var oDoc = document; var oEl1 = oDoc.createElement('div'); oEl1.id = 'myDiv'; var oEl2 = oDoc.createElement('span'); oEl2.id = 'mySpan'; var oEl3 = oDoc.createElement('font'); oEl3.innerText = 'Hello'; var oFrag = oDoc.createDocumentFragment(); oFrag.appendChild(oEl1); oFrag.appendChild(oEl2); oFrag.appendChild(oEl3); oDoc.body.appendChild(oFrag); | |
| Know the performance hits of methods and properties A few of the fundamental core objects, properties and functions are quite expensive in terms of memory and processing power. Two of the largest performance drains are "document.getElementById" and "element.innerHTML". When using "getElementById" it is important to cache the returned object otherwise the JavaScript engine will look up the document structure and locate the element each time it is called: Bad example: if ( document.getElementById( 'myElement' ) != null ){ if (document.getElementById( 'myElement' ).value != 'Hello'){ alert(document.getElementById( 'myElement' ).value ); } } Good example: var oElement = document.getElementById( 'myElement' ); if ( oElement != null ){ var sValue = oElement.value; if ( sValue != 'Hello' ){ alert( sValue ); } } When using "innerHTML" always buffer the HTML content before setting it back, otherwise the JavaScript engine will rebuild the document structure every time the HTML is updated: Bad example: for ( var iIndex = 0; iIndex < iANumber; iIndex++){ oMyElement.innerHTML += '<br>' + iIndex + '</br>'; } Good example: var sHtml = ''; for ( var iIndex = 0; iIndex < iANumber; iIndex++){ sHtml += '<br>' + iIndex + '</br>'; } oMyElement.innerHTML = sHtml; | |
| Use Ajax to render large client side structures If you are dynamically rendering a large amount of client-side HTML (such as loading a list of values depending of another value), use Ajax to render instead of the JavaScript engine. Even if you obey the performance improvements above, JavaScript is still slow at manipulating the DOM and creating large amounts of dynamic HTML. A simple Ajax method that creates an HTML string, followed by a JavaScript callback function that pushes the HTML into the innerHTML of a containing object, can be significantly faster. |
Preventing Memory Leaks |
| |
In JavaScript, these two types of memory leaks frequently occur:
Circular References Circular references are caused by references between DOM objects and JavaScript objects. The IE framework is built on COM. As such, the DOM and JS engines (and any DOM element) are COM objects with their own garbage collector. Therefore, references between different COM objects cannot be collected by the memory management routines... even when the browser is refreshed. Apparently, this is not a bug with IE. Rather, it is a design feature to increase stability. However, what it means is that unless you clear references on DOM objects to JS objects or other DOM objects, memory will be wasted. Examples are event handlers on input fields, and properties added to one DIV that point to another DIV. References: http://www.barelyfitz.com/screencast/javascript/memory-leak/ | ||||||
DOM Leaks DOM leaks occur when a DOM object is removed from the DOM, but stays in memory. This memory is freed when the page is refreshed, but can lead to the page quickly becoming unresponsive. Two causes are:
References: http://www.hedgerwow.com/360/dhtml/ie6_memory_leak_fix/ |
To see if your application contains memory leaks, you can use the free Process Explorer tool from Microsoft. This tool shows you memory in use by an application. If you examine the JavaScript application, you will notice that when you use the web page, the memory will increase. Then, after a refresh, not all memory is released:
To prevent such crippling memory leaks, follow these guidelines:
| Use the sapphire garbage collector The sapphire.garbage object can dispose of your DOM objects more effectively. When you want to dispose of a DOM object (or set of DOM objects) use the method: sapphire.garbage.add( oElement ) This will safely remove your Element from the page and place in a garbage bin object. Then, when the garbage bin meets its object limit (10 by default), it will call the collection routines to remove the objects from memory. If you are handling large objects (such as divs with a great deal of content), you can also force the garbage collector to run using: sapphire.garbage.collect() This method will also activate the DOM and JS engine collection routines, thus freeing memory. All of our API functionality now makes use of this feature. Accordingly, use this if you add new functionality that creates dynamic content on a page. | |
| Create DOM objects in a try-finally When you have a method to create a DOM object and return it, surround the return statement with a try-finally. This stops the circular references from the DOM object link being kept in memory. Example: var o = document.createElement('div'); try { return o; } o = null } | |
| Remove circular references When you are finished with your DOM object, remove any attributes you have added that are references to other objects. An example is a DIV element with which you add a link to a JS object or another DIV. The best methodology is to have a dispose method that accompanies your functionality. The dispose method should create the DOM object, then call the algorithm to null out any object attributes, then send to the garbage bin. | |
| Remove event handlers As with circular references, any events you attach to a DOM object will keep the DOM object in memory. Therefore, in the dispose method, use a detachEvent call to remove any events that have been added. Events that are added using the anonymous technique will not suffer the memory leak issue and do not need to be removed. | |
| Dispose any xml schema or extended class names An XML schema or extended class name is a style class that uses advanced imported XML namespaces and functionality such as "time" or any classes that uses behaviors. These offer linked functionality to other COM objects. Therefore, when a DOM object is disposed with a class name, the COM object still references it. The best course of action is to add the removal of any complex class names from the object before you send it to the garbage bin in your disposal method. |
Once you are managing memory correctly and the garbage collector is in place, you should see patterns where the memory increases until the garbage bin is full... then, memory is fully released. Also, a refresh should not keep any memory (below).
Here are some examples that cause memory leaks, along with their solutions:
Creation of DOM objects This will cause two memory leaks... a circular reference and a DOM leak: function createDiv() { var oDiv = document.createElement('div'); oDiv.id = 'mydiv'; return oDiv; } var oDiv = createDiv(); oDiv.innerHTML = 'some content'; document.body.appendChild(oDiv); oDiv.parentElement.removeChild(oDiv); To repair this leak, place a try...finally around the return from the method, and pass the element to be removed to the garbage collector. function createDiv() { var oDiv = document.createElement('div'); oDiv.id = 'mydiv'; try { return oDiv; } finally{ oDiv = null; } } var oDiv = createDiv(); oDiv.innerHTML = 'some content'; document.body.appendChild(oDiv); sapphire.garbage.add( oDiv ); oDiv = null; | ||
Circular references This relates two simple DOM objects so if something happens on one, it affects the other. Refreshing the page will leave all elements in memory: <div id=oPrimary style="display:block">title</div> <div id=oSecondary style="display:none">A sub title</div> <script> var oPri = document.getElementById( 'oPrimary' ); var oSec = document.getElementById( 'oSecondary' ); if ( oPri != null && oSec != null ) { oPri.relatedElement = oSec; oSec.relatedElement = oPri; oPri.onclick = openClose(); oSec.onclick = openClose(); } function openClose() { var oEl = event.srcElement; if ( oEl.style.display == 'block') { oEl.style.display = 'none'; oEl.relatedElement.style.display = 'block'; } } </script> What we must do is create a dispose function that is called when the page unloads. The dispose function must remove any circular references, free variables where required, and stop DOM objects being stored globally: <div id=oPrimary style="display:block">title</div> <div id=oSecondary style="display:none">A sub title</div> <script> function setup() { var oPri = document.getElementById( 'oPrimary' ); var oSec = document.getElementById( 'oSecondary' ); if ( oPri != null && oSec != null ){ oPri.relatedElement = oSec; oSec.relatedElement = oPri; oPri.onclick = openClose(); oSec.onclick = openClose(); oPri = null; oSec = null; } } function openClose() { var oEl = event.srcElement; if ( oEl.style.display == 'block') { oEl.style.display = 'none'; oEl.relatedElement.style.display = 'block'; } oEl = null; } function dispose() { var oPri = document.getElementById( 'oPrimary' ); var oSec = document.getElementById( 'oSecondary' ); if ( oPri != null && oSec != null ){ oPri.relatedElement = null; oSec.relatedElement = null; oPri.onclick = null; oSec.onclick = null; oPri = null; oSec = null; } } window.addEventListener( 'load', setup ); window.addEventListener( 'unload', dispose ); </script> For more examples, see: http://www.bazon.net/mishoo/articles.epl?art_id=824 http://www.hedgerwow.com/360/dhtml/ie6_memory_leak_fix/ http://support.microsoft.com/kb/830555 http://www.javascriptkit.com/javatutors/closuresleak/index.shtml |
Useful Utilities |
| |
As described in LabVantage JavaScript Utilities, LabVantage provides the Code Completion Utility and JavaScript Interrogator.