XPages SSJS Action Group - xpages

I have an action group on click of a button. First action, does some validation, second action throws up a confirm (Do you want to save?) And if yes, the third action goes off and does some other stuff.
The issue I have, is if validation fails on the first action, I don't want the other 2 actions to run, so I should get no confirmation etc.
If validation fails, I've tried doing a break, and a return false, but neither seem to work. I'm sure I'm missing something obvious, and suffering from Monday syndrome, but I can't seem to work it out!
Event handler code below, thanks:
<xp:eventHandler event="onclick" submit="true"
refreshMode="complete">
<xp:this.action>
<xp:actionGroup>
<xp:executeScript>
<xp:this.script><![CDATA[#{javascript:for (var i = 1; i < viewScope.rows+1; i++) {
print("Starting Array.....");
var fieldName:string = "ObjectiveSelfAssessment" +i;
var fieldName2:string = "ObjectiveDetails" +i;
print ("Field Name: " + fieldName);
var fieldValue = document1.getItemValueString(fieldName);
var fieldValue2 = document1.getItemValueString(fieldName2);
print ("Field Value: " + fieldValue);
if (fieldValue =="" || fieldValue==null){
print("Assessment Empty");
if(!fieldValue2 =="" || !fieldValue2 == null){
print("Objective Empty");
//Do validation
var o = {};
o.title = "Validation Failed";
o.body = "You must enter self assessment details for each objective";
o.alertIcon = "fa-thumbs-down fa-lg";
o.autoClose = true;
o.alertType = "danger";
requestScope.put("alertServer",o);
//requestScope.put("validated",false);
return false;
break;
}
}
}
}]]></xp:this.script>
</xp:executeScript>
<xp:confirm>
<xp:this.message><![CDATA[#{javascript:"Are you sure you want to submit your self assessment?"}]]></xp:this.message>
</xp:confirm>
<xp:executeScript>
<xp:this.script><![CDATA[#{javascript:document1.replaceItemValue("rows",viewScope.rows);
//document1.replaceItemValue("status","Self Assessment Completed");
document1.save();
var o = {};
o.title = "Document Saved";
o.body = "This document has been succesfully submitted";
o.alertIcon = "fa-thumbs-up fa-lg";
o.autoClose = true;
o.alertType = "success";
requestScope.put("alertServer",o);
}]]></xp:this.script>
</xp:executeScript>
</xp:actionGroup>
</xp:this.action>
</xp:eventHandler>
Update 1: Conditional code on action group:
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:actionGroup>
<xp:this.condition><![CDATA[#{javascript:for (var i = 1; i < viewScope.rows+1; i++) {
print("Starting Array.....");
var fieldName:string = "ObjectiveSelfAssessment" +i;
var fieldName2:string = "ObjectiveDetails" +i;
print ("Field Name: " + fieldName);
var fieldValue = document1.getItemValueString(fieldName);
var fieldValue2 = document1.getItemValueString(fieldName2);
print ("Field Value: " + fieldValue);
if (fieldValue =="" || fieldValue==null){
print("Assessment Empty");
if(!fieldValue2 =="" || !fieldValue2 == null){
print("Objective Empty");
var o = {};
o.title = "Validation Failed";
o.body = "You must enter self assessment details for each objective";
o.alertIcon = "fa-thumbs-down fa-lg";
o.autoClose = true;
o.alertType = "danger";
requestScope.put("alertServer",o);
print("FALSE");
return false;
break;
}else{
print("TRUE");
return true;
}
}
}
}]]></xp:this.condition>
<xp:confirm>
<xp:this.message><![CDATA[#{javascript:"Test11111"}]]></xp:this.message>
</xp:confirm>
<xp:executeScript>
<xp:this.script><![CDATA[#{javascript:document1.replaceItemValue("rows",viewScope.rows);
//document1.replaceItemValue("status","Self Assessment Completed");
document1.save();
var o = {};
o.title = "Document Saved";
o.body = "This document has been succesfully submitted";
o.alertIcon = "fa-thumbs-up fa-lg";
o.autoClose = true;
o.alertType = "success";
requestScope.put("alertServer",o);}]]></xp:this.script>
</xp:executeScript>
</xp:actionGroup>
</xp:this.action></xp:eventHandler>

Paul, I've accepted your answer reg doing more manual coding as that's the approach I've taken. I'm submitting an answer so I can show my solution using code formatting....
I now do my validation client side - The reason for getting the children etc, is I create/remove fields dynamically which are shown with repeat controls for my text box's, so I do not know the element id's....
var objcolparent = document.getElementById("ObjColOuter").children[0];
var sacolparent = document.getElementById("SAColOuter").children[0];
var rows = sacolparent.getElementsByTagName("TEXTAREA").length;
for (var i = 0; i < rows; i++) {
var saValue = sacolparent.getElementsByTagName("TEXTAREA")[i].value;
if(saValue ==""){
var objValue = objcolparent.getElementsByTagName("TEXTAREA")[i].value;
if(!objValue==""){
// Validation failed, do client side bootalert
var o = {};
o.title = "Validation Failed";
o.body = "You must enter self assessment details for each objective";
o.alertIcon = "fa-thumbs-down fa-lg";
o.alertType = "danger";
bootAlert.show('alertServer',JSON.stringify(o))
return false;
break;
}
}
}
if(confirm("Are you sure you want to submit your self assessment?")){
return true;
}else{
return false;
}
If validation is successful, it then continues to run my server side stuff:
document1.replaceItemValue("rows",viewScope.rows);
document1.replaceItemValue("status","Self Assessment Completed");
document1.save();
var o = {}
o.title = "Document Saved";
o.body = "This document has been succesfully submitted";
o.alertIcon = "fa-thumbs-up fa-lg";
o.autoClose = true;
o.alertType = "success";
requestScope.put("alertServer",o)
Thanks to everyone who contributed!

Action groups are useful for basic, minimum-coding actions. But, as you're seeing, "easier" point-and-click, configuration-driven actions mean flexibility is harder. Manual coding may seem harder, but makes flexibility easier. If you put a confirm action on an XPage, preview it, and look at the source, it will tell you what function you need to use for manual coding. When I looked at the XSP class about seven years ago, I think the function used was XSP.confirm() equivalent to a basic JavaScript confirm() call. Using that may make your code more readable and make flexibility easier.

You can add a condition to your actionGroup. Here's a simple example:
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:actionGroup condition="#{javascript:myCondition()}">
xp:confirm>
<xp:this.message><![CDATA[#{javascript:"Are you sure you want to submit your self assessment?"}]]></xp:this.message>
</xp:confirm>
<xp:executeScript>
...
</xp:executeScript>
</xp:actionGroup>
</xp:this.action>
</xp:eventHandler>
So in your case you just add your first action as a condition and have it return true or false depending on whether the logic in the action group must run or not.

There is a solution but it involves implementing a series of elements to properly support this scenario. But when it's up I believe it can be very helpful.
Your question is somewhat connected to a blog post of mine.
To help you in your specific case I will try to summarize here all the moving parts. Hope you like reading...
1st element: a server-side helper method
If you do validation outside of the validation phase there's no real way to let the client know something went wrong, or better there might be but it would be disconnected from an eventual flow you want to stay in (your case). What you want to do first, therefore, is to create a way to tell the client. First thing we build a static class with a simple helper method:
public enum Helper {
;
public static void setErrorHeader(HttpServletResponse response, PhaseId phaseId) {
response.setHeader("Application-Error", phaseId.toString());
}
}
We will later invoke this method to "flag" the validation as failed
2nd element: a client-side helper method
We need a JavaScript function that helps us tap into the response header we eventually add (add this function to your XPages through a JS library).
var helper = {
isBadRequest : function(xhr) {
return xhr.getResponseHeader("Application-Error") !== null;
}
}
3rd element: a beefed up event handler
To get things started in the proper way we must leverage the event handler parameters:
<xp:button id="myButton" value="Three-step action">
<xp:eventHandler
id="myEventHandlerId"
event="onclick"
submit="false"
action="#{myBean.makeItFlow}"
script="threeStep('validate', 'threeStepSave(arguments[1].xhr)')"/>
</xp:button>
It's very important for the eventHandler to have the attributes defined as written above:
id: used to grab the event handler later on
submit="false": doesn't submit the form, we will do it manually
action: contains the reference to the server side bean method that will be invoked. I used the name myBean.makeItFlow. I'm making the assumption you know how to use managed beans
script: where the "real" action takes place
At this point in the script attribute we call the XPages client-side javascript method that does the post - XSP.partialRefreshPost() - and we pass the various parameters manually. In this case I chose to avoid putting all the necessary script in-line with the event. At the bottom of the page you can add the following helper functions:
<xp:scriptBlock
value="
function threeStep(param, onCompleteFunc) {
var execId = '#{id:containerId}';
var refreshId = '#{id:containerId}';
var eventHandlerId = '#{id:myEventHandlerId}';
var opts = {
execId: execId,
params: { '$$xspsubmitid': eventHandlerId, action: param },
onError : 'console.log(arguments[0])'
};
if (onCompleteFunc) {
opts.onComplete = onCompleteFunc;
}
XSP.partialRefreshPost(refreshId, opts);
}
function threeStepSave(xhr) {
if (helper.isBadRequest(xhr)) {
return alert('Validation failed');
}
if (!confirm('Do you want to continue?')) return;
threeStep('save');
}" />
The method's first parameter is the id that will be refreshed. Then we have an object whose properties define:
param: a parameter we want to pass in order to tweak the server-side method behaviour
onCompleteFunc: the client side javascript that will be invoked at the end of the server side method evaluation (the js must be a string and will be evaluated)
var execId: the block (and therefore scope) that will be submitted with the POST
var refreshId: the block that will be refreshed
var eventHandlerId: the very important reference to the event handler ID
4th element: the bean action method
Now, by means of a Java bean we define all the action logic behind the button. I decided to go with 1 specific method that will react to the action parameter value passed by the event handler.
public void makeItFlow() {
FacesContext facesContext = FacesContext.getCurrentInstance();
ExternalContext externalContext = facesContext.getExternalContext();
String action = (String) externalContext.getRequestParameterMap().get("action");
if ("validate".equals(action)) {
// Your logic
boolean failed = false;
// When validation fails add the error response header
if (failed) {
HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
Helper.setErrorHeader(response, PhaseId.INVOKE_APPLICATION);
}
} else if ("save".equals(action)) {
// Let's save this thing
}
}
I am adding a brief video of how it should behave (in the example I added some additional code to make the various phases obvious)
https://youtu.be/JayzcGex-rQ

Related

Xpages - JS code fails to execute when clicked via a button

I have a strange issue, where I have a simple javascript button, that call's 1 function to write some document history and replace the value of one field. It then does a partial update of a container div ID.
This button works fine when accessed via the UK, however when used by Swiss colleagues, the button is doing nothing.
I have in 1 instance, witnessed it not working for 1 user, only for it to suddenly work later in the day, so I'm stumped as to what the issue may be.
Button Code
<xp:button id="btnProceed" styleClass="btn btn-primary" value="Proceed">
<xp:this.disabled><![CDATA[#{javascript:try{
if (getComponent("chkConfirmed").getValue() == "false") {
return true;
} else {
return false;
}
}catch(e){
openLogBean.addError(e,this);
}
}]]></xp:this.disabled>
<xp:eventHandler event="onclick" submit="true" refreshMode="partial"
refreshId="contentWhiteBackground">
<xp:this.action><![CDATA[#{javascript:var dt = new Date();
var arrHistory:array = AddIndepConfirmationHistoryItem(currentDocument, dt, "Confirmed understanding of the opening guidance", userBean.displayName);
document1.replaceItemValue("showIntroduction", "2");
document1.save();
}]]></xp:this.action>
</xp:eventHandler></xp:button>
Called code
Sub AddIndepConfirmationHistoryItem(confdoc As NotesDocument, dt As Variant, action As String, usrname As String)
Dim hlist As StringList
If Len(confdoc.History(0)) = 0 Then
confdoc.History = IndepConfirmationHistoryItem(dt, action, usrname)
Else
Set hlist = New StringList(confdoc.History, "")
Call hlist.Append(IndepConfirmationHistoryItem(dt, action, usrname))
confdoc.History = hlist.Items
End If
End Sub
EDIT:
function AddIndepConfirmationHistoryItem(doc, dt, action, username){
var ArrDocHistory:array = doc.getItemValueArray("History");
var dateTimeFormat = new java.text.SimpleDateFormat("dd/MM/yyyy kk:mm");
var dateTimeString = dateTimeFormat.format(dt);
if(ArrDocHistory.length < 1){
// This should always return an object as it is created when an objectives document is first
// created but do this check to be safe and create an array if for some reason it doesnt exist
ArrDocHistory = [dateTimeString+"|"+action+"|"+username];
}else{
// append new value to the array
ArrDocHistory.push(dateTimeString+"|"+action+"|"+username);
}
doc.replaceItemValue("History",ArrDocHistory);
doc.replaceItemValue("LastUpdatedByName",username);
doc.replaceItemValue("LastUpdatedDate",dt);
//doc.Save();
}

Getting value via getComponent on compositeData

A check is made on a input field during save, if value exit then it should pass. The input field value exist as a compositeData, but the check keeps failing.
Whats the best way of validating or getting the value of the input field?
I have tried getting this value using the getComponent function.
<xp:inputText id="fieldname" value="#{compositeData.dataSource.fieldname}">
<xp:this.attrs>
<xp:attr name="placeHolder" value="type here"></xp:attr>
</xp:this.attrs>
<xp:eventHandler event="onchange" submit="true" refreshMode="partial" refreshId="inputTextContainer" execId="fieldname">
<xp:this.action><![CDATA[#{javascript:try {
var vVal = getComponent("fieldname").getValue();
viewScope.put("fieldNameScope", vVal);
print("vVal: " + vVal);
}catch(e) {
print("Fieldname error: " + e.toString());
}
}]]></xp:this.action>
</xp:eventHandler>
</xp:inputText>
//var field = getComponent("fieldname").getSubmittedValue(); //returns null.
var field = viewScope.fieldNameScope; //getComponent("fieldname").getValue(); //returns nothing.
if(field == "" || field == null){
vContinue = false;
viewScope.MessageType = "Error";
viewScope.MessageText = "Please field is empty";
return context.redirectToPage( "home.xsp" );
}else{
vContinue = true;
}
I was expecting the check to pass if value exist in the field, but the actual result is null.
I stored the value into a viewscope and then call the viewscope instead of using the getComponent. I will update my question with the onchange eventhandler.

Xpages - Add a flag via through a button click on a repeat item

The cards above are populated via a repeat and the view is not categorised. What I want to add now is the ability to append a delete flag to any of the card in any order.
Below is the code for the delete button:
<xp:link>
<span class="glyphicon glyphicon-trash pull-right text-primary"></span>
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action><xp:actionGroup><xp:executeScript>
<xp:this.script><![CDATA[#{javascript:
var name=getComponent("FullName").getValue();
var vec:NotesView = database.getView("SupportTeam");
var docEv:NotesDocument = vec.getFirstDocument();
if (name == docEv.getItemValueString("FullName")) {
docEv.replaceItemValue("SupportAction", "Delete");
docEv.save();
}
}]]></xp:this.script>
</xp:executeScript>
</xp:actionGroup>
</xp:this.action></xp:eventHandler>
</xp:link>
The code above works, but the delete button need to be clicked twice for it work and it has to be in the order, that is if 'Test 6' is clicked it will not delete since 'Test 5' is in the way.
I tried using the getdocumentbykey() but the view will need to be categorised which will then show multiple entries. In this case, it will display lots of blank cards.
Your help will be appreciated.
We need to see your repeat code, however, aslong as you set the var property of your repeat to something, that then makes row data available to you, so you could use something like:
var id = rowData.getUniversalID();
var docEv:NotesDocument = database.getDocumentByUNID(id);
docEv.replaceItemValue("SupportAction", "Delete");
docEv.save();
//Or to do a hard delete
docEv.remove(true);
I decided to loop through the collection twice using the while loop to get the collection and then the for loop for the action and that seems to work, but I'm sure there should be a better way of doing this.
Below is the final code:
var name = getComponent("FullName").getValue();
var vec:NotesView = database.getView("SupportTeam");
var docEv:NotesDocument = vec.getFirstDocument();
var collection = [];
while (docEv != null){
try{
var member = docEv.getItemValueString("SupportFullName"), memberLength = collection.length;
collection.push(member);
for (var i = 0; i < memberLength; i++) {
if(memberLength != null && name == docEv.getItemValueString("SupportFullName")){
docEv.replaceItemValue("SupportAction", "Delete");
docEv.save();
}
}
}catch(e){
print("error: " + e.toString());
}
var tmp = vec.getNextDocument(docEv);
docEv.recycle();
docEv = tmp;
}
Thanks for all the response.

Xpages, Compute combo box value

I have a combo box its values are 10, 20, 30, 40, 50 when I create a document and display it in a view the number is appeared but I want to display its labels (labels are text) not numbers.
Can someone please help me find out how to solve this issue.
Thanks
I tend to add values of a given checkbox into a Resource Bundle, and then access that Resource Bundle both from the xp:selectItems and the xp:viewColumn values.
First, I'll create a Resource Bundle which I set as "label", with the following values:
itemsList_status=Draft|0,Published|1,Archived|-1
itemsList_delimiter=,
Here's an example of the xp:selectItems for an xp:combobox:
<xp:selectItems>
<xp:this.value>
<![CDATA[#{javascript:return getSelectItems('status');}]
</xp:this.value>
</xp:selectItems>
Here's my getSelectItems() SSJS function:
var getSelectItems : function(key) {
try {
var returnObj = new Array();
var items = null;
switch(key) {
case "status" :
items = #Text(label.itemsList_status).split(label.itemsList_delimiter);
break
}
for( var n=0, l=items.length; n < l; n++) {
returnObj.push(items[n]);
}
return returnObj;
} catch(e) {
print(database.getTitle() + " SSJS Error for getSelectItems()");
print(e.toString());
}
}
Run this function, and it'll return 3 options that read Draft, Published, and Archived while actually storing 0, 1, and -1 respectively. The problem that you're running into - and what using this abstracted method of storing your labels:values in a Resource Bundle + SSJS to address - is when you go to read the value but actually want to display the label...
Here's my getSelectedValue() function:
var getSelectedValue : function(key, thisValue) {
try {
var returnObj = new Array();
var items = null;
switch(key) {
case "status" :
items = #Text(label.itemsList_status).split(label.itemsList_delimiter);
break
}
var s = "";
var l = "";
for( var n=0, i=items.length; n < i; n++) {
if(items[n].indexOf("|") == -1) {
s = items[n];
l = items[n];
} else {
s = items[n].split("|").pop();
l = items[n].split("|").shift();
}
if(thisValue == s) {
return l;
}
}
return thisValue;
} catch(e) {
print(database.getTitle() + " SSJS Error for getSelectedValue()");
print(e.toString());
}
}
Now you'd pass the current value and the desired label:value Resource Bundle item handle, and this function spits out the correlating label. Here is how I use it in an xp:viewColumn:
<xp:viewColumn
id="viewColumn_status">
<xp:this.value><![CDATA[#{javascript:return ""}]]></xp:this.value>
<xp:text
value="#{javascript: return
getSelectedValue('status',thisEntry.getDocument().getItemValueString('status'))}" />
<xp:viewColumnHeader
value="#{label.viewColumnHeader_posts_status}"
id="viewColumnHeader_status">
</xp:viewColumnHeader>
</xp:viewColumn>
... and that's about it - the Resource Bundle entry to "itemList_status" acts as the authoritative master for the label:value pairs, so you just check that for the input building and deconstruct it for the labeling of stored values.
Hope this helps!
Using aliases is analog to the usage in a normal form.
Use a formula item for the values:
return ["- Please select -|", "ten|10", "twenty|20", "thirty|30"];
But keep in mind that the values that is stored is always TEXT and not a number.

Clicking on Navigator Entry hides my nodes

hen the xPage loads the render code works just fine. However, when the user clicks on Register for e-Statements they both get hidden until I refresh the page manually. I tried doing a full update and other parts of my code fail trying to access some java beans.
The code used for the render is a in my global javascript library.
Any thoughts?
<xe:navigator id="navigator1">
<xe:this.treeNodes>
<xe:basicContainerNode label="Register for e-Statements"
submitValue="RegisterForEstatements" enabled="true">
<xe:this.rendered><![CDATA[#{javascript:myEStatements.renderStatements()}]]>
</xe:this.rendered>
</xe:basicContainerNode>
<xe:basicLeafNode label="View My e-Statements" onClick="linkToEstatements();">
<xe:this.rendered><![CDATA[#{javascript:myEStatements.renderStatements()}]]>
</xe:this.rendered>
</xe:basicLeafNode>
</xe:this.treeNodes>
<xp:eventHandler event="onItemClick" submit="true"
refreshMode="partial" refreshId="navigator1">
<xp:this.action><![CDATA[#{javascript:
if( context.getSubmittedValue() == "RegisterForEstatements" )
{
sessionScope.put( "dialogAcceptEStatementTitle", "Accept e-Statements!" );
var dialogAcceptEstatements = getComponent( "dlgAcceptEStatements" );
dialogAcceptEstatements.show();
return "";
}
sessionScope.put( "dialogOopsTitle", "Oopps!" );
sessionScope.put( "dialogOopsMessage", "\nThis Feature Has Not Been Enabled Yet!" );
var dialogOops = getComponent( "dialogOops" );
dialogOops.show();
return "";
}]]>
</xp:this.action>
</xp:eventHandler>
}]]>
</xe:navigator>
Here is the additional code from the Javascript Library. FYI, the properProfile is a JavaBean that appears to be null when I click the navigator. I have defined the bean scope as request and tried session neither change the error.
var myEStatements =
{
// If the property receives Statements return true to display the e-Statement Links
// in the occ navigator
"renderStatements" : function()
{
var result = false;
try
{
result = aPropertyProfile.getStatementCoupon_1().equalsIgnoreCase( "Statement" );
} catch (e)
{
print(e.message);
}
return result;
},
// Fetch the e-Statement Accept Message
"getEStatementAcceptMessage" : function()
{
var eStatementAcceptMessage = "";
try
{
eStatementAcceptMessage = eStarService.getEStatementAcceptMessage();
} catch(e)
{
print( e.message );
}
return eStatementAcceptMessage;
}
}

Resources