Quickie: Referencing the prompt DOM element with the Cognos 10.2 Prompt API

With all of the nice things in the new API, there are still a few annoyances. For instance, the functions for adding values only work on textboxes, meaning you can’t programmatically add and remove elements. You also can’t easily move things around, or flip switches, or do any of the normal things you would do with Javascript.

Poking through the JS files, I did find a few things that should make life easier. Today I had an issue where I had to make a value prompt to allowing users the choice of viewing the report YTD or for the entire year. The cube has a nice hierarchy for that, with the captions yes/no or the all level for everything (yes, I know the cube could have been designed so that the No choice shows the entire year, this is a post about JS not OLAP so shush). The issue is that the users want to select “Everything” or “Until December 11”. The everything choice is fairly easy, it’s a static choice with the display set to “Everything”. The “Until December 11” is more difficult, the caption should change every day. Instead of altering the cube to put in a dynamic caption, I opted to use another static choice (this way I don’t need to go to the cube at all), and change the option text with JS.

In order to do this, I need to find the prompt on the page. The ID for the prompt itself is “PRMT_N19F64920x0B67C9E4RS” but that looks like it will change every time the report is run, how can I guarantee that I will always be able to access that. Keep in mind that I’m incredibly lazy, and don’t want to spend the time wrapping the prompt with my own uniquely identified div (because then I’d have to keep track of all of the div IDs, and make sure to move the HTML items when I move the prompt and it’s just too much of a hassle). Surely there must be an easier way.

It turns out that you can get the ID of the prompt with the Prompt API:

var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};
alert(acme.getControl('FilterType')._id_);

That would return N19F64920x0B67C9E4RS.

So that means that we can very easily modify the text of the prompt:

<script>
var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

Date.prototype.getMonthName = function() {
  var monthNames = [ 'January','February','March','April','May','June','July','August','September','October','November','December'];
  return monthNames[this.getMonth()];
}

var dt = new Date(),
    dtCaption = 'Until ' + dt.getMonthName() + ' ' + dt.getDate();

document.getElementById('PRMT_' + acme.getControl('FilterType')._id_).getElementsByTagName('OPTION')[1].dv = dtCaption;
document.getElementById('PRMT_' + acme.getControl('FilterType')._id_).getElementsByTagName('OPTION')[1].innerHTML = dtCaption;
</script>

This will change the display value (dv) and the label (innerHTML) with Until December 11 (or whatever the current date is).

Quickie: Changing default option in a Select and Search Prompt

I’ve been somewhat negligent in responding to the comments lately. One of the most common questions appears to be how to change the default option in a select and search prompt.

The following should work for all versions of Cognos.

Put an HTML item before the prompt, with the expression:

<div id="mySnS">

Now put another HTML item after the prompt with the expression:

</div>
<script>
/* Select and search options
 * 2 - Starts with any of these keywords * DEFAULT
 * 3 - Starts with the first keyword and contains all of the remaining keywords
 * 4 - Contains any of these keywords
 * 5 - Contains all of these keywords
 */
document.getElementById('mySnS').getElementsByTagName('input')[5].click();
</script>

The script will locate the mySnS element that surrounds the prompt, generate an array of the input tags, and click() on the one specified. In the example, 5 is selected so it will click on the sixth element (0 based array).

Going through the Cognos JavaScript files, it looks like there is supposed to be a function in the new Prompt API that would let you do it in an easier manner, unfortunately it appears to be an incomplete function, maybe it will be released in the new fixpack.

In theory, you should be able to do something like the following:

var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

acme.getControl('mySnS2').setProperty( "caseInsensitive", false);
acme.getControl('mySnS2').setProperty( "searchType", "containsAny");

But, as I said, the function doesn’t appear to be complete in this version. Use the other method for now.

Cognos Prompt API

Well, it’s been a week since I’ve returned from the IOD, and I’m almost fully recovered from the jet-lag. I had an absolute blast, and am looking forward to going back next year.

I promised more details on the various booths that I went to, and I do have the various papers that they gave me. I will eventually get to them! But before I do, this is what I started writing while I was at the IOD, but only now am I getting around to actually publishing it.

One of the most exciting new features in Cognos 10.2 is the introduction of a standardized prompt api. No longer will developers have to scramble to try to figure out how to get validate prompts at run time. Or how to repopulate them as needed through other interactions.

IBM has released a standard set of functions, and has even released a few samples containing them. The goal in this post is to learn how to use the new API and maybe to redo a few challenges I’ve had previously.

Just as previous version required a bit of JavaScript to prepare your page for playing with the prompt objects, this version is no different. What is different, is that instead of having to memorize the entire fW formWarpRequest nonsense, now it’s a much simpler:

var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

The acme namespace is to prevent function conflicts with existing Cognos functions, just preface all functions with that and you’ll be good. The “_THIS_” will automatically be replaced with the correct Cognos Namespace, so your JavaScript should still work if you’re running the report from Report Studio or the Cognos Connection. This never ever needs to change, so just copy and paste the above (although employees of Acme Inc may want to change the fictious namespace). This solution was shamelessly ripped from Rick Blackwell’s wonderful session at the IOD. All credit for anything and everything related to Cognos and JavaScript should obviously go to him now.

To begin with, let’s use Rick’s standard example for Postal Code validation.

 	/*
	 *
	 * This function will associate a validation function with the prompt control called promptPostalCode
	 *
	 */
	asdf.assignValidator = function ( ) {
		// Create the report object
		oCR = cognos.Report.getReport( "_THIS_" );

		// Create an array of prompt controls
		// This object is used in both functions in this script.
		asdf.promptControlPostalCode = oCR.prompt.getControlByName("promptPostalCode");

		// Make sure we have a valid Postal Code in the format "ana nan" such as K3F 5F4
		asdf.promptControlPostalCode.setValidator(asdf.postalCodeValidator);

	};

	/*
	 *
	 * This function is called for each character entered by the user.
	 * Ensure the value is a valid Postal Code (ie. A1A 1A1 with spaces) using RegEx.
	 *
	 */
	asdf.postalCodeValidator = function(promptValue) {
		//promptValue is an array of 1 value

		// If a value has been entered by the user ensure it matches the pattern
		if (promptValue.length > 0) {
			var rePostalCodeFormat = new RegExp("[a-z][0-9][a-z] [0-9][a-z][0-9]", "gi" );
			if ( rePostalCodeFormat.test(promptValue[0].use) ) {
				return true;
			} else {
			    return false;
			}
		} else {
		 	// If the prompt contains no value it is valid.
			// This is important as the prompt and filter are optional
			return true;
		}
	};

His example is a bit complicated, but it’s good to demonstrate the capabilities.

The first function finds the promptPostalCode prompt, and assigns the validator. Now when a user enters a value, it will trigger the validator function, passing the entered value.
The second function will take the entered value compare it against the regex, and return a true/false, telling Cognos whether the finish button should be active, and if there should be a red underline in the prompt object.

Of course, this is not the only thing that can be done with the validator function. The validator gives us an easy to use onchange function for the prompts.

A few of my clients want tree prompts which automatically hides after a user has selects a member. Previously, there were a few was this could be accomplished, each of them more difficult than the last. But now, instead of using the JS to validate, I can use the validate function to hide the prompt after a few seconds.

First I create an HTML item containing span with an ID:

<span id="treeLabel" onclick="acme.toggleTree()">Tree Prompt</span>

Next, I wrap the tree prompt inside a span with boxtype set to none:

<span id="treePrompt" style="display:none">

</span>

First a function to directly toggle the display of the tree prompt:

acme.toggleTree = function()
{
document.getElementById('treeprompt').style.display=document.getElementById('treeprompt').style.display=='none'?'block':'none';
}

Next, the actual validation function:

/*
 * function hideTree. Paul Mendelson - 2012-10-22
 * Whenever a tree node is clicked, it will wait {acme.hideTimer} seconds before hiding
 * the tree. If another selection is made, it will restart the counter.
 */
acme.hideTree= (function () {
  var timer;
  var elm;
  return function (){
    clearTimeout(timer);
  var promptName = this.getName();

    //standard way of determing source of click.
    var targ;
    if (!e) var e = window.event;
    if(!e) return false;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
      targ = targ.parentNode;

    timer = window.setTimeout(function() {
      document.getElementById('treePrompt').style.display='none';
      document.getElementById('treeLabel').innerHTML = acme.getControl('Time').getValues()[0].display;
    },acme.hideTimer);
    return true;
    };
})();

Notice that it’s not actually doing any validation; it’s simply returning true. Notice that it’s also sending the label of the selected node to the tree label. Ultimately the new Prompt API makes it significantly easier to build complex reports.

Example XML:

<report xmlns="http://developer.cognos.com/schemas/report/9.0/" useStyleVersion="10" expressionLocale="en-us">
				<modelPath>/content/folder[@name='Samples']/folder[@name='Cubes']/package[@name='Sales and Marketing (cube)']/model[@name='2008-07-25T15:28:38.072Z']</modelPath>
				<drillBehavior modelBasedDrillThru="true"/>
				<layouts>
					<layout>
						<reportPages>
							<page name="Page1">
								<style>
									<defaultStyles>
										<defaultStyle refStyle="pg"/>
									</defaultStyles>
								</style>
								<pageBody>
									<style>
										<defaultStyles>
											<defaultStyle refStyle="pb"/>
										</defaultStyles>
									</style>
									<contents><textBox parameter="Parameter1" name="textBox" multiSelect="true"/><HTMLItem description="Label">
			<dataSource>
				<staticValue>&lt;span id="treeLabel" onclick="acme.toggleTree()"&gt;Tree Prompt&lt;/span&gt;
&lt;span id="treePrompt" style="display:none"&gt;</staticValue>
			</dataSource>
		</HTMLItem><selectWithTree parameter="Time" refQuery="Time" name="Time"><selectWithTreeItem refDataItem="Time"/></selectWithTree><HTMLItem description="scripts">
			<dataSource>
				<staticValue>&lt;/span&gt;

&lt;script&gt;

/*
 * Fictious Namespace and getter. Don't change these unless IBM says so or you know what you're doing.
 * acme is now the official standard fictious namespace (thanks Rick), so make sure all functions sit there.
 * This is based on the samples, so I'm giving all credit to Rick Blackwell of IBM.
 */
var acme = {};
acme.getControl = function(promptName)
{
  var ocr = cognos.Report.getReport("_THIS_");
  return ocr.prompt.getControlByName(promptName);
};

/*
 * Global Params
 * Paul Mendelson 2012-28-10
 * These control the behaviour of the functions below.
 */

//Time in miliseconds to wait before prompt disappears.
acme.hideTimer = 1000;

/*
 *  This will return any available methods or functions or whatever available on the referenced object.
 */
function getMethods(myObject)
{
  var funcs=[]
  for(var name in myObject)
  {
    funcs.push(name)
  }
  return  funcs.join(', ');
}

/*
 * Hide the prompt when validating
 */

/*
 * function hideTree. Paul Mendelson - 2012-10-22
 * Whenever a tree node is clicked, it will wait {acme.hideTimer} seconds before hiding
 * the tree. If another selection is made, it will restart the counter.
 */
acme.hideTree= (function () {
  var timer;
  var elm;
  return function (){
    clearTimeout(timer);
  var promptName = this.getName();

    //standard way of determing source of click.
    var targ;
    if (!e) var e = window.event;
    if(!e) return false;
    if (e.target) targ = e.target;
    else if (e.srcElement) targ = e.srcElement;
    if (targ.nodeType == 3) // defeat Safari bug
      targ = targ.parentNode;

    timer = window.setTimeout(function() {
      document.getElementById('treePrompt').style.display='none';
      document.getElementById('treeLabel').innerHTML = acme.getControl('Time').getValues()[0].display;
    },acme.hideTimer);
    return true;
    };
})();

acme.toggleTree = function()
{
  document.getElementById('treeprompt').style.display=document.getElementById('treeprompt').style.display=='none'?'block':'none'
}

/*acme.checkPostalCode = function()
{
  var rePostalCodeFormat = new RegExp( "[a-z][0-9][a-z] [0-9][a-z][0-9]", "gi" );
  if(rePostalCodeFormat.test(this.getValue())) return true;
  return false;
}
*/
acme.checkPostalCode = function()
{
  var rePostalCodeFormat = new RegExp( '^[0-9]{0,3}$', "gi" );
  if(rePostalCodeFormat.test(this.getValue())) return true;
  return false;
}
acme.getControl('textBox').setValidator(acme.checkPostalCode);
acme.getControl('Time').setValidator(acme.hideTree);
&lt;/script&gt;

</staticValue>
			</dataSource>
		</HTMLItem></contents>
								</pageBody>
							</page>
						</reportPages>
					</layout>
				</layouts>
			<XMLAttributes><XMLAttribute name="RS_CreateExtendedDataItems" value="false" output="no"/><XMLAttribute name="listSeparator" value="," output="no"/><XMLAttribute name="RS_modelModificationTime" value="2008-07-25T15:28:38.133Z" output="no"/></XMLAttributes><queries><query name="Time"><source><model/></source><selection><dataItem name="Time" aggregate="none"><expression>rootMembers([sales_and_marketing].[Time].[Time])</expression></dataItem></selection></query></queries><reportName>Paul - Tree prompt</reportName></report>