I have created a nice custom control for an input text box for US currency. I want to be able to put this CC on a page multiple times, so I have made the field name a property. Everything seems to work right if there is one control on a page, but if there are more than that then the code in the CC is not working as intended. I am trying to do some validation and editing in CSJS inside the CC, and trying to get the unique computed ID when I do so. But it is not working - the first value overwrites the second value, and other weird things happen.
My code is below:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xc="http://www.ibm.com/xsp/custom"
xmlns:xe="http://www.ibm.com/xsp/coreex"
createForm="false">
<style> .errorRed{ border: 2px solid red; }</style>
<xp:inputText
id="curText1"
value="#{viewScope.a}"
styleClass="pull-right"
style="width:200px;text-align:right"
defaultValue="0.00">
<xp:this.converter>
<xp:convertNumber
type="currency"></xp:convertNumber>
</xp:this.converter>
<xe:this.dojoAttributes>
<xp:dojoAttribute
name="input"
value="text-align: right">
</xp:dojoAttribute>
</xe:this.dojoAttributes>
<xp:eventHandler
event="onchange"
submit="false">
<xp:this.script><![CDATA[//Set some things
var thisID = '#{javascript:getClientId("curText1")}';
var thisCmp = XSP.getElementById(thisID);
var thisVal = XSP.getElementById(thisID).value;
//Error if this is not a number
if (isNaN(thisVal))
{thisCmp.className = thisCmp.className + " errorRed";
return}
else
{thisCmp.className = thisCmp.className.replace(" errorRed","")}
//Must remove $ and any commas
y = thisVal.replace(',','');
z = y.replace('$','');
//Must fix to 2 decimal places
if ((typeof z) === 'string'){
z = parseFloat(z).toFixed(2)}
else {
z = z.toFixed(2)
}
//Now put it back in the field
thisID.value = parseFloat(z);
XSP.partialRefreshPost(thisID);]]></xp:this.script>
</xp:eventHandler>
</xp:inputText>
</xp:view>
I am adding my modified code:
Custom Control:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xc="http://www.ibm.com/xsp/custom"
xmlns:xe="http://www.ibm.com/xsp/coreex"
createForm="false">
<style>.errorRed{ border: 2px solid red; }</style>
<xp:scriptBlock
id="scriptBlock1"
type="text/javascript">
<xp:this.value><![CDATA[formatNumber = function(thisID) {
var thisCmp = XSP.getElementById(thisID);
var thisVal = XSP.getElementById(thisID).value;
//Must remove currency symbol and any commas
y = thisVal.replace(',','');
z = y.replace('$','');
//Error if this is not a number
if (isNaN(z))
{thisCmp.className = thisCmp.className + " errorRed";
return}
else
{thisCmp.className = thisCmp.className.replace(" errorRed","")}
//Must fix to 2 decimal places
if ((typeof z) === 'string')
{z = parseFloat(z).toFixed(2)}
else
{z = z.toFixed(2)}
//Now put it back in the field
thisID.value = parseFloat(z);
XSP.partialRefreshPost(thisID,{execId:thisID, immediate: true});
}]]></xp:this.value>
</xp:scriptBlock>
<xp:inputText
id="curText1"
styleClass="pull-right"
style="width:200px;text-align:right"
defaultValue="0.00"
value="#{compositeData.price}">
<xp:this.converter>
<xp:convertNumber
type="currency"
currencySymbol="$">
</xp:convertNumber>
</xp:this.converter>
<xp:this.dojoAttributes>
<xp:dojoAttribute
name="input"
value="text-align: right">
</xp:dojoAttribute>
</xp:this.dojoAttributes>
<xp:eventHandler
event="onblur"
submit="false">
<xp:this.script><![CDATA[var thisID = '#{javascript:getClientId("curText1")}';
formatNumber(thisID);]]></xp:this.script>
</xp:eventHandler>
</xp:inputText>
<xp:inputText>
<xp:this.value><![CDATA[#{javascript:{"${"+compositeData.bla+"}"}}]]></xp:this.value>
</xp:inputText>
</xp:view>
Here is the code for the Xpage, with 2 custom controls:
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xe="http://www.ibm.com/xsp/coreex"
xmlns:xc="http://www.ibm.com/xsp/custom">
<xp:panel>
<xp:this.data>
<xe:objectData
var="doc">
<xe:this.createObject><![CDATA[#{javascript:var doc = new com.scoular.model.Project();
var unid = sessionScope.get("key");
if (unid != null) {
doc.loadByUnid(unid);
} else {
doc.create();
}
sessionScope.put("key",null);
return doc;}]]></xe:this.createObject>
</xe:objectData>
</xp:this.data>
</xp:panel>
<xp:panel id="numbers">
<xc:cc_CommonInputCurrency2
field="total1"
bla="#{doc.prjAmtColumn11}">
</xc:cc_CommonInputCurrency2>
</xp:panel>
<xp:panel id="panel1">
<xc:cc_CommonInputCurrency2
field="total2"
bla="#{doc.prjAmtColumn12}">
</xc:cc_CommonInputCurrency2>
</xp:panel>
</xp:view>
Your value property is bound to viewscope.a. In the rendering phase that scope has one and one value only, so all your fields are bound back to the same.
The better way is to use a parameter in your custom control to provide the value. So you would have something like <cc:moneyControl bla="document1.price">
Then you use a field you give the CSS attribute hidden and bind it to #{"${"+compositeData.bla+"}"} and let your dojo update that field. Saves you a server trip and having an extra field outside the CC.
How it works: the $ gets evaluated once and first and forms any valid data source, not only documents.
Hope that helps
I see two issues, first the code:
<xe:this.dojoAttributes>
<xp:dojoAttribute
name="input"
value="text-align: right">
</xp:dojoAttribute>
</xe:this.dojoAttributes>
should be using <xp:this.dojoAttributes> :
<xp:this.dojoAttributes>
<xp:dojoAttribute
name="input"
value="text-align: right">
</xp:dojoAttribute>
</xp:this.dojoAttributes>
Second, your isNaN check should come after your strip out the dollar sign and comma, like this:
//Must remove $ and any commas
y = thisVal.replace(',','');
z = y.replace('$','');
//Error if this is not a number
if (isNaN(z))
{
thisCmp.className = thisCmp.className + " errorRed";
return
}
else
{
thisCmp.className = thisCmp.className.replace(" errorRed","");
}
//Must fix to 2 decimal places
if ((typeof z) === 'string'){
z = parseFloat(z).toFixed(2)}
else {
z = z.toFixed(2)
}
If you need to have the field bound to a scoped field, one sure way to have a dynamic "private" variable inside a custom control is to name it using the clientId of from one of its components.
Example:
Create a panel that wraps the content of the custom control. Set an id on the panel to wrapper.
To set the scoped variable:
viewScope.put( getClientId( 'wrapper' ), 'someValue' )
To get the scoped variable:
viewScope.get( getClientId( 'wrapper' ) )
If you need access to the scoped variable outside the custom control, you could specify the client id of a component in the XPage as field name (compositeData) for the custom control using the same logic.
Related
I have an XSL/XML/JS file. It was written by someone who is not working here any more, and I normally only deal with SQL, so Im at a loss as to how to achieve what I need to do
Im trying to add some variables into the file within the existing CDATA block. I then use the variables within a function. However, I have tried the below and variations of this, but keep getting a syntax error within the application (Dynamics AX). Am I doing something obviously wrong here, with either how I am declaring the variables or how I am using them? These are the only changes I have made, and without these changes there are no syntax or any other issues/errors.
<?xml version="1.0" encoding="utf-16"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:mxm="http://schemas.microsoft.com/dynamics/2008/01/documents/MxmServInterfaceOutboundAif"
xmlns:data="http://www.example.com/data" exclude-result-prefixes="xs xsi xsl">
<xsl:output method="text" encoding="UTF-8" indent="no" />
<msxsl:script language="JScript" implements-prefix="data">
<![CDATA[
//Minor Repairs email address
var MinorsEmail = xxx#domain.com
//Service Dept email address
var ServiceEmail = yyy#domain.com
//Major Repairs email address
var MajorsEmail = zzz#domain.com
//Select appropriate email to use
function EmailFrom(fault)
{
var type = fault.substr(0,2);
if (type == "MI")
{
var ret = MinorsEmail;
}
else
{
var ret = concat(ServiceEmail, "; ",MajorsEmail);
}
return ret;
}
Edit: Adding quotes around the variable values has solved part of the problem. The problem now is that the CONCAT does not function as intended. I get the following error now:
Variable concat has not been declared
Thanks to #Martin Honnen, the answer was to add quote to variable values, and to use + instead of CONCAT:
<?xml version="1.0" encoding="utf-16"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:mxm="http://schemas.microsoft.com/dynamics/2008/01/documents/MxmServInterfaceOutboundAif"
xmlns:data="http://www.example.com/data" exclude-result-prefixes="xs xsi xsl">
<xsl:output method="text" encoding="UTF-8" indent="no" />
<msxsl:script language="JScript" implements-prefix="data">
<![CDATA[
//Minor Repairs email address
var MinorsEmail = "xxx#domain.com"
//Service Dept email address
var ServiceEmail = "yyy#domain.com"
//Major Repairs email address
var MajorsEmail = "zzz#domain.com"
//Select appropriate email to use
function EmailFrom(fault)
{
var type = fault.substr(0,2);
if (type == "MI")
{
var ret = MinorsEmail;
}
else
{
var ret = ServiceEmail + "; " + MajorsEmail;
}
return ret;
}
I have been reviewing the postings online, as well as the postings by IBM and I'm still confused about how to get my form to submit.
I am using formvalidation.io validation and from CSJS, I get whether the validation is correct or not. Then, I want to submit if it is correct, and not submit if it isn't.
I've tried different variations but get the same result. If the validation is NOT correct, the form correctly continues to display and doesn't submit. However, if the validation IS correct, the form does not submit. I receive the error message XSP.executeOnServer is not a function and have tried various ways to resolve this without success. Here's my code:
<xp:button value="Join Our Team" id="button1">
<xp:eventHandler event="onclick" submit="false">
<xp:this.script><![CDATA[$(document).ready(function() {
$(this).click(function(){
$("form").data('formValidation').validate();
var isValidForm = $("form").data('formValidation').isValid();
if (isValidForm){
XSP.executeOnServer('#{id:eventhandler1a}', '#{id:panel1}');
}
else {
$("form").data('formValidation').validate();
return false;
}
});
});]]></xp:this.script>
<xp:this.action><![CDATA[#{javascript:XSP.executeOnServer = function () {
// the event handler id to be executed is the first argument, and is required
if (!arguments[0])
return false;
var functionName = arguments[0];
// OPTIONAL - The Client Side ID that is partially refreshed after executing the event handler
var refreshId = (arguments[1]) ? arguments[1] : "#none";
var form = (arguments[1]) ? XSP.findForm(arguments[1]) : dojo.query('form')[0];
// catch all in case dojo element has moved object outside of form...
if (!form)
form = dojo.query('form')[0];
// OPTIONAL - Options object containing onStart, onComplete and onError functions for the call to the
// handler and subsequent partial refresh
var options = (arguments[2]) ? arguments[2] : {};
// OPTIONAL - Value to submit in $$xspsubmitvalue. can be retrieved using context.getSubmittedValue()
var submitValue = (arguments[3]) ? arguments[3] : '';
// Set the ID in $$xspsubmitid of the event handler to execute
dojo.query('[name="$$xspsubmitid"]')[0].value = functionName;
dojo.query('[name="$$xspsubmitvalue"]')[0].value = submitValue;
this._partialRefresh("post", form, refreshId, options);
} }]]></xp:this.action>
</xp:eventHandler>
</xp:button>
<xp:panel id="panel1">
<xp:eventHandler event="onClick" submit="true" id="eventhandler1a" refreshMode="complete">
<xp:this.action>
<xp:saveDocument></xp:saveDocument>
</xp:this.action>
</xp:eventHandler>
</xp:panel>
I think your best bet would be to use RPC, which will process the CS and return a go/no-go value to your SSJS to submit or not. Below is a full XPage you can use to see how it works. This just populates a viewScope, but I think it will give you how it works in a very simple way.
<?xml version="1.0" encoding="UTF-8"?>
<xp:view xmlns:xp="http://www.ibm.com/xsp/core" xmlns:xe="http://www.ibm.com/xsp/coreex">
<xp:br></xp:br><xp:br></xp:br>
<xp:button value="CSJS" id="button2">
<xp:eventHandler event="onclick" submit="false">
<xp:this.script><![CDATA[myRPC.myMethod("somevalue").addCallback(function(response) {
alert("the response is " + response);
});]]></xp:this.script>
</xp:eventHandler></xp:button>
<xp:br></xp:br><xp:br></xp:br><xp:br></xp:br><xp:br></xp:br><xp:br></xp:br><xp:br></xp:br>
<xe:jsonRpcService id="jsonRpcService1" serviceName="myRPC"
state="true">
<xe:this.methods>
<xe:remoteMethod name="myMethod">
<xe:this.arguments>
<xe:remoteMethodArg name="myArg" type="string"></xe:remoteMethodArg>
</xe:this.arguments>
<xe:this.script>
<![CDATA[
print("invocation: " + myArg);
viewScope.invokedBy = myArg;
print("viewScope:" + viewScope.invokedBy);
return viewScope.invokedBy;
]]>
</xe:this.script>
</xe:remoteMethod>
</xe:this.methods>
</xe:jsonRpcService>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:text escape="true" id="computedField1" value="#{viewScope.invokedBy}"></xp:text>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:button value="Refresh" id="button1">
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:confirm>
<xp:this.message><![CDATA[#{javascript:return "is: " + viewScope.invokedBy;}]]></xp:this.message>
</xp:confirm>
</xp:this.action></xp:eventHandler></xp:button>
<xp:br></xp:br>
<xp:br></xp:br></xp:view>
If CSJS in an eventHandler returns true, SSJS will fire. If it returns false, it won't. So this.script just needs to return true.
However, you're then posting CSJS in this.action. That property of the eventHandler takes SSJS, so dojo.query and XSP.findForm won't work. You need to put SSJS in there to save your document or whatever else you need to do.
This is one of the spooky things that happens from time to time with Domino. Now my code works with a simple return true or return false. I had done this in the beginning and it was a real head scratcher why it wouldn't work. I've had these type of things happen from time to time with Domino and it's always scary when you don't know why they occur. But I'm happy that my clean, simple code works now.
I have two field both on the same custom control:
Gender & Title.
Gender is a required field and starts off blank.
Title is depended on Gender and starts of Disabled.
Once Gender is selected, it should populate the options for Title and enable the field.
But, this is also Bootstrap. So, I want to change the Gender field to show it is correct by changing the field from "has-warning" to "has-success" and displaying the glyph "glyphicon-ok".
I also need to enable the Title field and remove the "glyphicon-warning-sign".
I might be doing this wrong, but I am using Client-Side Javascript vs Server-Side with a partial refresh.
Related Fields Gender and Title
My first solution was to combine a bit of Server-Side with Client-Side and then calling a script library.
<xp:panel id="ApplicantsGender_pnl" styleClass="form-group has-feedback has-warning">
<xp:label id="ApplicantsGender_Label1" value="Gender:"
for="ApplicantsGender"
styleClass="col-sm-4 control-label field-label">
</xp:label>
<div class="input-group col-sm-8">
<xp:panel styleClass="input-group-addon">
<span class="glyphicon glyphicon-asterisk requiredfield"></span>
</xp:panel>
<xp:radioGroup id="ApplicantsGender"
value="#{document1.ApplicantsGender}" styleClass="form-control">
<xp:selectItem itemLabel="Male" itemValue="MALE"></xp:selectItem>
<xp:selectItem itemLabel="Female"
itemValue="FEMALE">
</xp:selectItem>
<xp:eventHandler event="onclick" submit="false">
<xp:this.script><![CDATA[var FieldID = '#{javascript:getClientId("ApplicantsGender")}';
var FormGroupPnlID = '#{javascript:getClientId("ApplicantsGender_pnl")}';
var FormGroupGlyphID = '#{javascript:getClientId("ApplicantsGender_glyph")}';
var TitleID = '#{javascript:getClientId("ApplicantsTitle")}';
var MaleTitlesID = '#{javascript:getClientId("TitlesMale")}';
var FemaleTitlesID = '#{javascript:getClientId("TitlesFemale")}';
field_ApplicantsGender_xp(FieldID,FormGroupPnlID,FormGroupGlyphID,TitleID,MaleTitlesID,FemaleTitlesID);
]]>
</xp:this.script>
</xp:eventHandler>
</xp:radioGroup>
<xp:panel id="ApplicantsGender_glyph" styleClass="glyphicon form-control-feedback"></xp:panel>
</div>
<xp:text escape="true" id="TitlesMale" value="#{viewScope.TitlesMale}" style="display:none"></xp:text>
<xp:text escape="true" id="TitlesFemale" value="#{viewScope.TitlesFemale}" style="display:none"></xp:text>
</xp:panel>
function field_ApplicantsGender_xp(FieldID , FormGroupPnlID , FormGroupGlyphID , TitleID , MaleTitlesId , FemaleTitlesId)
{
var gender=radio_getCheckedValue( document.getElementsByName(FieldID));
var applicantsTitle=document.getElementById(TitleID)
if (gender!=null)
{
if (gender=="MALE"){
var Titles = document.getElementById(MaleTitlesId);
} else {
var Titles = document.getElementById(FemaleTitlesId);
}
var Choices = Titles.innerHTML.split(',');
var select = document.getElementById(TitleID);
//Clear the Titles Options List
var i;
for(i = select.options.length - 1 ; i >= 0 ; i--)
{
select.remove(i);
}
//Set the Titles Options List
for (var i= 0, n= Choices.length; i < n ; i++) {
opt = document.createElement("option");
opt.value = Choices[i];
opt.textContent = Choices[i];
select.appendChild(opt);
}
select.disabled = false
//Set Bootstrap Successful
document.getElementById(FormGroupPnlID).setAttribute("class" , "form-group has-feedback has-success");
document.getElementById(FormGroupGlyphID).setAttribute("class" , "glyphicon form-control-feedback glyphicon-ok");
var TitleGlyphID = TitleID+"_glyph";
document.getElementById(TitleGlyphID).setAttribute("class" , "glyphicon form-control-feedback");
//alert(ApplicantsGenderID);
}
This does what I need it to do, but this is just the first of many fields that will need validation.
So, I was wondering if there was a way to do all the field ID translations just once up in a global area either on the page or in the custom control instead of having to do it within each onchange event.
I want to be able to create the script variables on the fly so I have them available for Client-Side scripting.
i.e. Someplace globally accessable:
var GenderID = '#{javascript:getClientId("ApplicantsGender")}';
var TitleID = '#{javascript:getClientId("ApplicantsTitle")}';
var MaleTitlesID = '#{javascript:getClientId("TitlesMale")}';
var FemaleTitlesID = '#{javascript:getClientId("TitlesFemale")}';
Then when I call the validation formula in the script library I just need to pass in the IDs involved.
i.e.
field_ApplicantsGender_xp(GenderID,TitleID,MaleTitlesID,FemaleTitlesID);
The application itself has many fields all with a bit of cross interaction.
Some of the interaction is within the same custom control and some is across custom controls.
I'm finding much of the Bootstrap validation is Client-Side. So with Xpages changing all the IDs, this has just become a bit more tricky.
Thanks for your help on this and I look forward to your feedback.
You can use a scriptBlock to define your global variables:
<xp:scriptBlock type="text/javascript">
<xp:this.value><![CDATA[
var GenderID = '#{javascript:getClientId("ApplicantsGender")}';
var TitleID = '#{javascript:getClientId("ApplicantsTitle")}';
var MaleTitlesID = '#{javascript:getClientId("TitlesMale")}';
var FemaleTitlesID = '#{javascript:getClientId("TitlesFemale")}';
]]></xp:this.value>
</xp:scriptBlock>
You would be polluting the global namespace with all those ID variables which is considered a bad practice so consider using a closure.
I am having a static html page on local storage(hard disk drive). I am not using any kind of server here. Along with this I have an xml file named testng.xml which I need to edit from html on button click using javascript.
I need to replace the <include> tag with <exclude> tag or vice versa based on user selection through a checkbox on web page. And then execute the testng xml by a button click.
The function is called from the html button as below
<input type="submit" onclick="editTestNG()" value="Execute Selected Tests">
Given below is my code for editing the xml.
<script type="text/javascript">
function editTestNG()
{
xmlHttp = new XMLHttpRequest();
xmlHttp.open("GET", "testng.xml", false);
xmlHttp.send();
xmlDoc = xmlHttp.responseXML;
txt = "";
x = document.getElementsByName("check");
document.write(x.length + "<br>"); // x.length is number of checkboxes on html page
z = xmlDoc.getElementsByTagName("run")[1];
y = z.children;
document.write(y.length + "<br>"); // y.length is the number of xml child nodes
document.write(x.length + "<br>"); // x.length is number of checkboxes on html page
for(i=0;i<x.length;i++)
{
document.write("abc");
if(x[i].type == 'checkbox')
{
document.writeln("def");
if(x[i].checked == "true" && y[i].nodeName == "exclude")
{
document.writeln("ghi");
txt = y[i].getAttribute("name");
newTag = xmlDoc.createElement("include");
newTag.setAttribute("name",txt);
p = z.replaceChild(newTag, y[i]);
document.write(p.nodeName+"<br>")
document.write(newTag.nodeName + "::"+ newTag.getAttribute("name")+"<br>");
}
if(x[i].checked == "false" && y[i].nodeName == "include")
{
document.writeln("jkl");
txt = y[i].getAttribute("name");
newTag = xmlDoc.createElement("exclude");
newTag.setAttribute("name",txt);
p = z.replaceChild(newTag, y[i]);
document.write(p.nodeName+"<br>")
document.write(newTag.nodeName + "::"+ newTag.getAttribute("name")+"<br>");
}
}
}
y = z.children;
for(i=0;i<y.length;i++)
{
document.writeln("<br>"+y[i].nodeName + " ## "+ y[i].getAttribute("name")+"<br>");
}
}
</script>
The output on html page after clicking the button is as below
140
7
0
exclude ## Group_CCMS_Login
exclude ## Group_CCR_Create_New
exclude ## Group_CCR_Create_New_EngType_M
exclude ## Group_CCR_Create_New_EngType_P
exclude ## Group_Full_ID_Search
include ## Group_Run_Query
exclude ## Group_Dup_Undup
I am able to retrieve the xml file and elements in the z and y variables. Also in the 2nd for loop, the output containing xml node names and attributes is printed. But after getting xml data in y variable, I am not able to get or use any html page elements such as checkboxes.
The value of x.length is 140 initially and y.length is 7, but then x.length becomes 0 and my 1st for loop does not execute.
I am unable to understand why I can't access html elements after getting xml data.
I never tried but as per my understanding you can create a bash file and then exucute that bash file using jsp code. Add that jsp code in the HTML.
Refer:-
executing a bash script from a jsp page
Now as you said "I need to prepare a user interface in HTML with checkboxes to select or deselect the tests to run and execute them with click of a button"
For that may you need to create different testng suite and accordingly you need to create a more bash respectively.
Refer:-
http://grokbase.com/t/gg/selenium-users/139r0nszp3/how-to-run-multiple-xml-files-with-testng
Situation
I'm using a custom error XPage, based highly off of the XSnippet from Tony McGuckin. It works rather well but I would like for the browser to execute a client-side JavaScript block (or load and run a JS file from a given URL). If I navigate directly to the custom error XPage, it loads correctly, but given the nature of how it loads on redirect from a SSJS runtime error, it seems to load any attempts at loading a script block in the head tag, inside the body tag. I've attempted passing through a JS script tag in the body (shown in the code below), attempted using the xp:headTag inside xp:resources, and attempted via an xp:script tag in xp:resources.
Browser's Perspective
From the browser's perspective, after encountering a runtime error during an event that invokes SSJS during a partial refresh, the xhr being invoked returns with a 500 and sets the content into the body tag (screen shot).
When viewing the response contents, the entire custom error XPage is there, including the <script type="text/javascript">console.log("hello world");<script>. This does not seem to trigger or put anything out to the JS console of the browser. What is visible via the JS console is some garbage from dojo complaining about getting back an XHR with response code of 500 (my dojoConfig is set to isDebug: true via xsp.client.script.dojo.djConfig in XSP Properties).
Question
Is there a way to get a client-side JS script tag to load and execute in the browser after an error 500 which occurs during the loading of a custom error XPage?
Here's the code for my Error page. To reproduce my results, invoke an SSJS action resulting in a runtime error (such as the ErrorOnClick XPage included in the OpenLog Logger for XPages project from Paul Withers) with a partial refresh event.
Error.xsp (set as the error page in XSP Properties)
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.ibm.com/xsp/core xsdxp://localhost/xsp~core.xsd"
pageTitle="${javascript:database.getTitle() + ' | Error'}">
<style
type="text/css"><![CDATA[
body {
background-color: lightblue;
}
form {
width: 1000px !important;
margin: 0 auto !important;
background-color: white !important;
margin-top: 2rem !important;
padding: 0.5rem !important;
height: auto;
}
.xspTextLabel {
font-weight: bold !important;
}
]]></style>
<img
class="logo-simple"
src="//placehold.it/124x32" />
<xp:panel>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:label
style="font-weight:bold;font-size:12pt"
value="An Unexpected Error Has Occurred:">
</xp:label>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:label
value="Error:"></xp:label>
<xp:br />
<xp:text
escape="false">
<xp:this.value><![CDATA[#{javascript:if( !!requestScope.error ){
var output = (requestScope.error.toString() || null)+"<br /><br />";
if(requestScope.error instanceof com.ibm.xsp.exception.XSPExceptionInfo){
var codeSnippet = requestScope.error.getErrorText();
var control = requestScope.error.getErrorComponentId();
var cause = requestScope.error.getCause();
output += "In the control : " + control + "<br /><br />";
if(cause instanceof com.ibm.jscript.InterpretException){
var errorLine = cause.getErrorLine();
var errorColumn = cause.getErrorCol();
output += "At line " + errorLine;
output += ", column " + errorColumn + " of:<br />";
}else{
output += "In the script:<br />";
}
if( #Contains(codeSnippet,"#{javascript:") ){
var snipAr = codeSnippet.split("#{javascript:");
var tmpSnip = snipAr[1];
var nwSnip = tmpSnip.substring(0, tmpSnip.length - 1);
output += "#{javascript:<br /><pre>"+nwSnip+"</pre>}"
}else{
output += codeSnippet;
}
}
return output;
}else{
return "";
}}]]></xp:this.value>
</xp:text>
<xp:br></xp:br>
<xp:br></xp:br>
<xp:label
value="Stack Trace:"></xp:label>
<xp:br />
<xp:text
escape="false"
style="font-size:10pt">
<xp:this.value><![CDATA[#{javascript:if( !!requestScope.error ){
var stackTrace = "";
var trace = (requestScope.error.getStackTrace() || null);
if(trace != null){
for(var i = 0; i < trace.length; i++){
stackTrace += trace[i] + "<br/>";
}
return "<pre>"+stackTrace+"</pre>";
}else{
return "nothing";
}
}else{
return "";
}}]]></xp:this.value>
</xp:text>
</xp:panel>
<script
type="text/javscript">
<![CDATA[console.log("Hello world...");]]>
</script>
</xp:view>
For what it's worth: I didn't find anything explicitly on this subject via a search of Google or StackOverflow.
UPDATE 1:
This was a case of either caffeine deprivation or just not seeing the forest through the trees. It helps to not use a CDATA block in your HTML code. The lazy developer in me tried copying and pasting between an xp:script block and the HTML <script> block, preserving it. Now for the public shaming of buying Marky beer in Atlanta.
UPDATE 2:
Marky's beverage of choice may be in peril. While I seem to have had issues with copying a CDATA tag in, the issue remains. In my efforts to produce a simplified page with a button to error out (loosely based on the above mentioned XPage from the OpenLog Logger for XPages ErrorOnClick.xsp), I mistakenly took out a part of what was causing my issues in the first place, the partial refresh. When I do a full refresh, no issue, but when I do a partial, it doesn't load. I'm enclosing a sample page to trigger an error, with two buttons; one to induce a full, the other a partial. SO, with a full refresh, I get an alert of "hello world...", with the partial, no dice.
MakeSomeError.xsp
<?xml version="1.0" encoding="UTF-8"?>
<xp:view
xmlns:xp="http://www.ibm.com/xsp/core">
<xp:panel
id="somePanel">
<xp:button
value="Failing Partial"
id="button1">
<xp:eventHandler
event="onclick"
submit="true"
refreshMode="partial"
refreshId="somePanel">
<xp:this.action><![CDATA[#{javascript:var a:NotesDateTime = null;
viewScope.myStuff = a.toJavaDate().toDateString();}]]></xp:this.action>
</xp:eventHandler>
</xp:button>
<xp:button
value="Failing Full"
id="button2">
<xp:eventHandler
event="onclick"
submit="true"
refreshMode="complete">
<xp:this.action><![CDATA[#{javascript:var a:NotesDateTime = null;
viewScope.myStuff = a.toJavaDate().toDateString();}]]></xp:this.action>
</xp:eventHandler>
</xp:button>
<xp:br />
<xp:text
value="#{viewScope.myStuff}" />
</xp:panel>
</xp:view>
UPDATE 3:
Okay. Sven's second answer has me very close, but for some reason I can't extrapolate just enough to get what I want to happen to occur. I'm including a GIF below of my results. The only thing different I would like to have happen is for my Error.xsp (custom error XPage) to continue loading after I encounter the error (it seems like I'll need to change the beforeRenderResponse block to an afterRenderResponse script perchance?). I want to append the script, not replace the Error.xsp loading. Basically, I'm trying to run a script after the error XPage is loaded (there's a helper JS file I'm trying to load into my custom error XPage, CSS is loading fine, just not the JS lib). I would love to:
get this working
share what it is (it's kind of cool, if I do say so myself)
Don't use CDATA in your script tags.
<script>
alert('hi Marky!');
</script>
Works for me.
OK, last try. A very interesting one. On your Error.xsp, add the following image:
<xp:text
escape="true"
id="executeOnAjax"
tagName="img">
<xp:this.attrs>
<xp:attr
name="src"
value="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==">
</xp:attr>
<xp:attr
name="onload"
value="alert('Hello World!');this.parentNode.removeChild(this);">
</xp:attr>
</xp:this.attrs>
<xp:this.rendered>
<![CDATA[#{javascript:
var ex = facesContext.getExternalContext();
var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
var refreshId = pMap.get("$$ajaxid");
refreshId?true:false;}]]>
</xp:this.rendered>
</xp:text>
if this doesn't fit your requirements, I don't understand what you are trying to accomplish.
The reason for this is behaviour is that this is a security feature. Browsers don't execute <script> blocks if they where loaded via Ajax.
But there is a workaround:
First you have to add a div for replacement to your calling XPage:
<div id="errRefresh" />
This is just a placeholder for the partial refresh, otherwise it will fail.
Now, you have to modify your error page to handle the partial refreshs. To do this, you have to detect if it is a refresh or not, but you cannot use the build-in functionality (it is nulled in an error page). So you have to do this by your own:
var ex = facesContext.getExternalContext();
var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
var refreshId = pMap.get("$$ajaxid");
Now you must set the response status code to 200, otherwise the error method from the event is called:
var resp:com.ibm.xsp.webapp.XspHttpServletResponse = facesContext.getExternalContext().getResponse();
resp.setStatus(200);
Then, you can add your CSJS block which must look like this:
<!-- XSP_UPDATE_SCRIPT_START -->
<script>
alert('Hello World!');
</script>
<!-- XSP_UPDATE_SCRIPT_END -->
When the parial refresh is processed, the refreshed DOM element is replaced, that's why we have to resend the HTML markup with the response, and overwrite the X-XspRefreshId to force replacement of our error element instead:
resp.setHeader('X-XspRefreshId', 'errRefresh' );
Last but not least, we have to skip the JSF lifecycle:
facesContext.responseComplete();
That's it.
Here is the complete code for the beforeRenderResponse event of the error page:
<xp:this.beforeRenderResponse>
<![CDATA[#{javascript:
var ex = facesContext.getExternalContext();
var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
var refreshId = pMap.get("$$ajaxid");
if( refreshId ){
var resp:com.ibm.xsp.webapp.XspHttpServletResponse = ex.getResponse();
var writer:java.io.PrintWriter = resp.getWriter();
writer.write( "<!-- XSP_UPDATE_SCRIPT_START -->\n" );
writer.write( "<script>\n");
writer.write( "alert('Hello World!' );\n" );
writer.write( "</script>\n");
writer.write( "<!-- XSP_UPDATE_SCRIPT_END -->\n" );
writer.write( "<div id=\"errRefresh\" />\n");
resp.setStatus(200);
resp.setHeader('X-XspRefreshId', 'errRefresh' );
facesContext.responseComplete();
}
}]]>
</xp:this.beforeRenderResponse>
Keep in mind that this might result in security issues.
Marky has another good idea: Hijack the response.
This could look like this:
<script>
if( !dojo._xhr )
dojo._xhr = dojo.xhr;
var myHandler = function(){
var xhrObj = arguments[1].xhr;
var response = xhrObj.response;
var header = xhrObj.getResponseHeader('X-XspRefreshId');
if( header == "#error" ){
eval( response );
}else{
arguments[1]["error"]( arguments[0], arguments[1]);
}
}
dojo.xhr = function(){
try{
var args = arguments[1];
args["failOk"] = true;
args["error"] = myHandler;
arguments[1] = args;
}catch(e){}
dojo._xhr( arguments[0], arguments[1], arguments[2] );
}
</script>
The beforeRenderResponse event has to be modified like this:
<xp:this.beforeRenderResponse>
<![CDATA[#{javascript:
var ex = facesContext.getExternalContext();
var pMap = com.ibm.xsp.util.TypedUtil().getRequestParameterMap(ex);
var refreshId = pMap.get("$$ajaxid");
if( refreshId ){
var resp:com.ibm.xsp.webapp.XspHttpServletResponse = facesContext.getExternalContext().getResponse();
var writer:java.io.PrintWriter = resp.getWriter();
writer.write( "alert('Hello World!' );\n" );
resp.setHeader('X-XspRefreshId', '#error' );
facesContext.responseComplete();
}
}]]>
</xp:this.beforeRenderResponse>