Using JavaScript in Oracle CPQ
Overview
What
JavaScript is a client-side interpreted language. It is available in most browsers, and can be used to augment a site's functionality.
Why
The decision to do custom development on a site is one that should never be made lightly. The risks that non-standard work introduces to the long-term success of a project are significant, and the implications and downsides of the decision can be tough to predict or plan for. Before considering JavaScript as an option, make sure to perform a proper risk assessment. Include all the stakeholders of the project in the discussion, be aware of the risks, and open a discussion to pursue alternative options.
Considerations:
- Custom Development is a form of tribal knowledge. It creates the need for a specialist in an otherwise standardized system.
- Upgrades to the Oracle CPQ application may conflict with your add-on work.
- Bugs in custom development work may have serious implications, including data corruption and loss.
- Custom work may interfere with "Standard" Oracle CPQ functionality, including admin tasks and troubleshooting.
Where
JavaScript should be written within the JavaScript Framework. The Framework has a few primary objectives:
- Centralize all JavaScript in the File Manager.
- Avoid the creation of global variables.
- Load code only on the page that it will be run.
- Minimize the impact of bugs in JavaScript on the rest of the site.
- Minimize the effort involved in disabling or removing the code in the future.
How
Before you begin: Learn JavaScript! Though it shares many characteristics of other C-style languages, JavaScript has many language features that can be surprising, and can cause serious issues if misused. Some great resources:
- is a good JavaScript tutorial for beginners.
- is a good resource for web-development best practices.
- is a great CSS, HTML, and JavaScript reference.
Administration
Build for failure
When approaching a JavaScript solution to a problem, it is important to consider that there are many different variables that could cause your features to break. In fact, between Browser compatibility issues and upgrades to CPQ, it is unreasonable to treat anything in the HTML of a page as constant or immutable. To deal with this uncertainty, design code that fails gracefully. This means different things in different situations. Consider the following code:
function getPrice() {
var value = document.getElementById("netPrice_quote").value;
return value;
}
If this code is run on a page that doesn't have "netPrice_quote"
available, then it will throw a TypeError
, because getElementById
will return null. This means that changes to the layout of the page will cause the entire Oracle CPQ application to break. You can deal with this by using jQuery to select the element. If netPrice_quote
isn't on the layout, then this code will not throw an error:
function getPrice() {
var value = jQuery("#netPrice_quote").val();
return value;
}
But just because the code doesn't throw an error, doesn't necessarily mean it is failing gracefully. It's possible that not having the net price will create serious problems for the application, such as data loss or corrupted quotes. If this is the case, then a loud, complete failure may be appropriate:
function getPrice() {
var value = jQuery("#netPrice_quote").val();
return value;
}
function usePriceForCalculation() {
var price = getPrice();
//if net price is not available, don't allow this script to run and warn the user
if(price === undefined) {
jQuery("body").prepend("<h2>There is an error on this quote. Please contact your system administrator.</h2>");
throw new Error("Custom work per Case #00012345. Net Price was not available for a critical calculation.");
}
// ...otherwise use it
}
Don't use inline JavaScript
JavaScript is largely an event-driven language. You can bind behavior to a web page in the form of loading events, click events, change events, and more. Many resources will recommend that you use "inline" JavaScript to accomplish this. For example:
<body onload="myJavascriptFunction()">
<input id="my-button" type="button" onclick="myJavascriptFunction()"/>
<select id="my-select" onchange="myJavascriptFunction()"/>
<a id="my-link" href="javascript:myJavascriptFunction()">
This method of event binding violates industry best practice, and creates code that is difficult to maintain and modify. The preferred way to accomplish this task is to select the elements you want to fire an event on, and then bind the event from within your code. Using jQuery, the above examples would be accomplished like this:
jQuery(document).ready(myJavascriptFunction)
jQuery("#my-button").click(myJavascriptFunction);
jQuery("#my-select").change(myJavascriptFunction);
jQuery("#my-link").click(myJavascriptFunction);
This approach has the clear benefit of separating your logic from your data, which makes it easier to maintain, and also allows you to centralize all of your code into a single file. It also means that the function being called doesn't have to be globally available.
It is intentional that code cannot be called from <a onclick="...">
Scope all variables
When you use or define a variable in JavaScript, it is very important not to create new globals, or global variables. Global variables introduce maintenance issues and unpredictability to your code, and can also conflict with other JavaScript that already exists on the page. These conflicts are difficult to detect and can have subtle and surprising results. In order to avoid creating globals, there are a few things you have to be aware of:
Use the var keyword when declaring new variables
Variables declared without the var keyword will not be scoped, no matter where they are in the program. This is bad, because you risk overwriting or colliding with other global variables that already exist on the page. And if a variable of the same name does not exist, then you will have inadvertently exposed your variable to the global namespace.
function createsGlobals() {
var non_global = "This variable will only be available within the createsGlobals function";
is_global = "This variable will be global, or will overwrite existing variables of the same name";
}
Functions are the only blocks with scope
In many other C-like languages, anything within { and } blocks are scoped. This is not the case in JavaScript.
if(true) {
var is_global = "This variable will be available outside the if block, even though I used 'var'";
// this function will be available from outside the if block
function global_function() {
var safe = "This variable is private to global_function. Functions are the only blocks with scope.";
}
//variables i and ii will be globally available
for(var i = 0, ii = 10; i<ii; i++) {
var is_global = "This variable will overwrite the previous is_global, even though I used 'var'";
}
}
jQuery.ready, require, and anonymous functions
The JavaScript Framework uses RequireJS to manage dependencies and scope. In terms of scope management, it is similar to putting code into jQuery's "ready" function, in that as long as you use the var keyword properly none of your code will be available globally:
// none of the variables in this example will collide, even though they have identical names
jQuery(document).ready(function() {
var safe = "scoped variable";
var scoped_function = function() { }
});
require([], function() {
var safe = "a different scoped variable";
var scoped_function = function() { }
});
The same effect can be accomplished with an anonymous function. Just put () after the closing } bracket to fire the function immediately:
(function() {
var inner = "scoped variable";
require([], function() {
var inner = "a different scoped variable";
});
}());
This example creates no new global variables.
Declare your variables at the beginning
A good way to deal with confusing scope within JavaScript is to declare all of your variables at the top of each function, using a single comma-separated "var" statement:
function sum_array(arg) {
var result, i, ii;
for(i=0, ii=arg.length; i<ii; i++) {
result += arg[i];
}
return result;
};
Treat native objects as private
You will occasionally find suggested solutions where a JavaScript native object is extended with a new method. You might see something like:
// DANGEROUS! Don't do this:
//adding a method to the "String" object...
String.prototype.trim = String.prototype.trim || function() { }
//extending the "Array" object...
Array.prototype.remove = Array.prototype.remove || function() { }
Within the Oracle CPQ application, this is inappropriate. This creates fragile and difficult to maintain code, and there is a risk of a conflict with other libraries and code. If you want to use JavaScript functions that exist in some browsers and not in others, write a "wrapper" function rather than extending the object:
(function() {
function is_array(obj) {
// Use the native function if it exists
if(typeof Array.isArray === "function") {
return Array.isArray(obj);
}
// my own custom code to test for array goes here
// ...
}
if(is_array(new Array())) {
//do stuff...
}
}());
Treat Oracle CPQ JavaScript as private
The Oracle CPQ Development group has written a lot of JavaScript that is available publicly in the global namespace. These functions should be treated as a private interface: do not call them, and do not modify them. Development may choose at anytime to re-factor, rewrite, or otherwise improve their functionality. If they do so, then any code that you have written that relies on those functions will suddenly stop working, and it may be very difficult to recreate the functionality.
Use brackets
JavaScript lets you skip the brackets for one-line blocks:
if(true) return;
for(var i=0, ii=arr.length; i<ii; i++)
count+=1;
Don't do this! This is a maintenance hazard, and makes it very easy for the next programmer to do this:
if(true) return;
for(var i=0, ii=arr.length; i<ii; i++)
count+=1;
// oops! we are no longer in the loop!
alert(arr[i]);
Use jQuery for DOM selection and manipulation
jQuery provides robust, dependable methods for binding events, selecting HTML elements, and modifying pages. When it is available, use it. For more information, see the topic Using jQuery in CPQ.
Use functions and objects
When using JavaScript's flexible functions and objects, your code can become descriptive and maintainable. Consider the example of a script that populates billing information in a commerce quote, based on data from a delimited string. It may look something like this:
//this example uses require.
//see the JavaScript Framework for more information.
require([], function() {
require.ready(function() {
var delimited_string, delimiter, ship_array, ship_object = {}, $shipping_form, i, ii, temp;
delimited_string = jQuery("#1_billingDelimitedString_quote").val();
delimiter = jQuery("#1_billingDelimiter_quote").val();
//bail out if we can't find our data
if(!delimited_string || !delimiter) { return; }
ship_data = delimited_string.split(delimiter);
for(i=0, ii=ship_data.length; i<ii; i++) {
//create a dictionary of key/values
}
//populate the fields accordingly
$shipping_form = jQuery("form[name='shipping']");
jQuery("input[name='name']", $shipping_form).val(ship_object["name"]);
//... and so on
});
});
The problem with this code is that one function has three distinct jobs: get the data, parse the data, and present the data. By splitting this up, the code becomes much more flexible:
require([], function() {
var addresses = {
parse_data: function(str, delimiter) {
// return the parsed data
},
get_parsed_data: function(data_selector, delim_selector) {
//gets the strings, and pass them to parse_data
},
fill_form: function($form, object) {
// populate the form
}
string_to_form: function(data_selector, delim_selector, form_selector) {
var data, $shipping_form;
$shipping_form = jQuery(form_selector);
data = addresses.get_parsed_data(data_selector, delim_selector);
addresses.fill_form($shipping_form, data);
}
};
require.ready(function() {
addresses.string_to_form("#1_shippingDelimitedString", "#1_shippingDelimiter", "form[name='shipping']");
});
});
Now within require.ready, we call string_to_form
, which does a pretty good job of describing the goal of the app. And by setting parameters in the functions, we can point at different data sources and forms:
...
require.ready(function() {
// use the same code to populate both shipping and billing info
addresses.string_to_form("#1_shippingDelimitedString", "#1_shippingDelimiter", "form[name='shipping']");
addresses.string_to_form("#1_billingDelimitedString", "#1_billingDelimiter", "form[name='billing']");
});
...
Use === and !==
JavaScript has two types of comparison operators: double and triple. Briefly, the difference is that the double operator attempts to cast the type of the compared values, while the triple operator does not. This can lead to some surprises. For instance:
false == '0'; //true
'' == '0'; //false
0 == '0'; //true
0 == ''; //true
null == false; //false
null == undefined; //true
The rules behind this type-casting are unusual, and not very memorable. Use the === and !== operators to remove ambiguity:
false === '0';// false
'' === '0'; //false
0 === '0'; //false
0 === ''; //false
null === false; //false
null === undefined; //false
Test your code
Your JavaScript will be running in a lot of different environments, and it's important to test it in every environment that your users will be running. The Framework provides as a way to test your code.
Troubleshooting
Errors & exceptions
It can be tempting to use try/catch blocks as a way to shield the user from errors. This can make it very difficult to locate the source of a problem in the future.
(function() {
try {
my_array[3].obj.value = "Uh-oh, this is dangerous.";
} catch(e) {/* Do nothing - just hide it */}
}());
The problem with try/catch is that it is often far too broad; it can cover up errors that you didn't anticipate would occur. Instead of throwing away exceptions, take steps to avoid specific errors. If other issues come up, then it will be much easier to single them out and deal with them.
(function() {
if(my_array.length < 4) { return; }
if(!my_array[3].obj) { return; }
my_array[3].obj.value = "This is more specific, and easier to troubleshoot.";
}());
I can't call my code from the console!
Code within the Framework is non-global, which means that directly calling the function from the console is not an option. To expose the code, you have two options.
- You can call "require" from the console:
//directly in the console...
require(["my_module"], function(module) {
//test module directly:
module.some_function();
});
- You can "export" your code to the global namespace from within the File Manager. Be sure to add a flag, as this is only appropriate for testing purposes:
//within the file manager...
require([], function() {
var addresses = {}, debug = true;
addresses.parse_data = function() {...}
...
if(debug === true) {
window.addresses = addresses;
}
});
The above example will make "addresses" globally available when the debug variable is set to true. This will allow you to do this directly in the console:
addresses.parse_data();
Notes
Using a Web Developer tool, set break points within your code. When you reload the page, you will be able to step through the code at the point you chose.
Related Topics
See Also