Using JavaScript in Oracle CPQ

Overview

ClosedWhat

JavaScript is a client-side interpreted language. It is available in most browsers, and can be used to augment a site's functionality.


ClosedWhy

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:

  1. Custom Development is a form of tribal knowledge. It creates the need for a specialist in an otherwise standardized system.
  2. Upgrades to the Oracle CPQ application may conflict with your add-on work.
  3. Bugs in custom development work may have serious implications, including data corruption and loss.
  4. Custom work may interfere with "Standard" Oracle CPQ functionality, including admin tasks and troubleshooting.

ClosedWhere

JavaScript should be written within the JavaScript Framework. The Framework has a few primary objectives:

  1. Centralize all JavaScript in the File Manager.
  2. Avoid the creation of global variables.
  3. Load code only on the page that it will be run.
  4. Minimize the impact of bugs in JavaScript on the rest of the site.
  5. Minimize the effort involved in disabling or removing the code in the future.

ClosedHow

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:

  1. Eloquent JavaScript is a good JavaScript tutorial for beginners.
  2. HTML Dog is a good resource for web-development best practices.
  3. Mozilla Developer Network is a great CSS, HTML, and JavaScript reference.

Administration

ClosedBuild 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

}


ClosedDon'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.


ClosedScope 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:

ClosedUse the var keyword when declaring new variables

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";

}


ClosedFunctions are the only blocks with scope

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'";

}

}


ClosedjQuery.ready, require, and anonymous functions

// 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() { }

});

(function() {

var inner = "scoped variable";

require([], function() {

var inner = "a different scoped variable";

});

}());


ClosedDeclare 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;

};


ClosedTreat 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...

}

}());


ClosedTreat 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.


ClosedUse 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]);


ClosedUse 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.


ClosedUse 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']");

});

...


ClosedUse === 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


ClosedTest 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 QUnit as a way to test your code.


Troubleshooting

ClosedErrors & 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.";

}());

ClosedI can't call my code from the console!

  • 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;

}

});

addresses.parse_data();


Notes

For more information regarding Oracle CPQ and the JavaScript Framework, see GS.COE.JA.18 - JavaScript Framework 2.0 Upgrade Kit.

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

Related Topics Link IconSee Also