Client-Side Javascript

Progammatically opening a mailto link from the Notes client

Our company has been transitioning from Notes mail to Outlook for a little while. One of the hurdles for me has been getting Notes to send mail when the user no longer has a Notes mail file and also wants to send any editable emails via the Outlook client. Today, I’d like to have a look at my latest solution to opening those editable emails in the Outlook client.

If you try using @URLOpen or ws.URLOpen, you’ll find that despite having set the default email to Outlook, Notes insists on opening a Notes document to send the mail. If you put the mailto link into a browser, it opens in the default mail client, Outlook, formatted properly. If you programmatically open a http link, it opens in the default browser. Yet, frustratingly, it won’t take that same mailto link that works in the browser and open it from LotusScript or a formula. So, we have to cheat.

mailto syntax

As a quick review, mailto links are actually very simple.

There are five components to the link, but none are required.

mailto:person@company.com Simply list the recipients email addresses. Outlook seems to prefer that you separate them with semi-colons, though most syntax guides suggest commas.

The other four are passed as parameters in the query string. So, before you use any of them, you have to use a question mark to separate the query string from the URL, then separate each parameter with an ampersand (&).

cc=joe@company.com or bcc=jill@company.com You can add carbon-copy folks or blind-copy folks in the same way.

subject=That%20issue The subject line should be encoded so that there are no spaces and any unusual characters pass through to the email rather than disrupt the syntax of the query string.

body=The%20contents%20of%20email The body is, of course, the most important thing to us and we can simply compute the values to be used. Links back to Notes documents to be opened in the Notes client can even be used, so long as you provide a proper notes:// URL.

Hard coded example

If we create a HTML document, store it locally and open it programmatically, it will execute any javascript we’ve got on the page as browsers ought to do. So, we can make it open the mailto link if our page has the following:

<script type="text/javascript">
subject = "Change Order for your approval";
body = "Your approval has been requested for changes made... ";
body = body + "%0A%0A";
body = body + "Please review these changes and approve, provide comments, or request more time to review within five business days of this notification. Otherwise, the change will be considered approved as per DAI policy.";
body = body + "%0A%0A";
body = body + "The pending change approval form and links to draft documents can be found here: ";
body = body + "%0A";
body = body + "Notes:///852580E9007624A0/0/B82A2F4ABF0D56818525808400601DBE";
	
mailtoString = "mailto:david_navarre@company.com?subject=" + subject + "&body=" + body;

window.open(mailtoString)</script>

Sample code

So, all we have to do is generate the mailtoString, put it into a new HTML document and open it via NotesUIWorkspace.URLOpen to get our document to open and popup the Outlook client, populated with the correct information.

Sub Click(Source As Button)
	Dim ws As New NotesUIWorkspace
	Dim session As New NotesSession
	Dim thisdb As NotesDatabase
	Dim uidoc As NotesUIDocument
	Dim approver As Variant
	Dim approverNames As String
	Dim i As Integer

	Set thisdb = session.CurrentDatabase
	Set uidoc = ws.CurrentDocument
	Set doc = uidoc.Document

	For i = 1 To 6
		approver = doc.getItemValue( "Approver"&i )
		If i = 1 Then
			approverNames = approver (0)
		Else
			If ( approver (0) <> "" ) Then
				approverNames = approverNames & ";" & approver (0)
			End If
		End If
	Next
	Dim subject As String
	Dim body As String
	Dim mailtoString As String
	Dim changeOrderNumber As Variant

	changeOrderNumber = doc.getItemValue ("CONum" )
	subject = "Change Order " & changeOrderNumber (0) & " for your approval in " & thisdb.Title
	subject = Replace ( subject, " ", "%20" )
	body = "Your approval has been requested for changes made using " & changeOrderNumber (0) & " in " & thisdb.Title
	body = body & "%0A%0A"
	body = body & "Please review these changes and approve, provide comments, or request more time to review within five business days of this notification. Otherwise, the change will be considered approved as per DAI policy."
	body = body & "%0A%0A"
	body = body & "The pending change approval form and links to draft documents can be found here: "
	body = body & "%0A"
	body = body & "Notes:///" & thisdb.ReplicaID & "/0/" & doc.UniversalID
	body = Replace ( body, " ", "%20" ) 

	mailtoString = "mailto:" & approverNames & "?subject=" & subject & "&body=" & body

	Dim fileName As String
	Dim dataDirectoryPath As String
	Dim url As String
	Dim fileNumber As Integer

	fileNumber = 1

	dataDirectoryPath = session.Getenvironmentstring("Directory", True)
	fileName = dataDirectoryPath & "\mailto.htm"

	Open fileName For Output As fileNumber

	Print # fileNumber, {<script type="text/javascript">}
	Print # fileNumber, {mailtoString = "} & mailtoString {"}
	Print # fileNumber, {window.open(mailtoString)</script>}

	Close # fileNumber

	url = "file:///" & fileName
	Call ws.Urlopen(url)

End Sub
Advertisement
Categories: Client-Side Javascript, Old Notes | Tags: , , , | 4 Comments

Are you sure? Asking for confirmation in #XPages

Often, we want to confirm with the user that they actually want to save or submit a document in XPages. I thought it would be very simple to customize the server-side simple action ‘confirm’ to include client-side data that the user had just entered, but that was not yet saved to disk.

So, I had what I thought was some simple and straight-forward SSJS:

<xp:confirm>
    <xp:this.message><![CDATA[#{javascript:var baseText = "Are you sure that you want to set the exchange rate for ";
        var effectiveDate = getComponent("effectiveDate").getValue().toString();
        var localCurrency = getComponent("localCurrency").getValue();
        var exchangeRate = getComponent("exchangeRate").getValue();
        return baseText + localCurrency + " to " + exchangeRate + " as of " + effectiveDate + "?"; }]]>
    </xp:this.message>
</xp:confirm>

I posted my question on StackOverflow and Paul Withers pointed out that I wasn’t going to get what I was looking for….

You’re computing SSJS to pass to a CSJS confirm() message. I would expect it to display values at the last refresh, not values just entered by the user. If you want the latest values, I think you’ll need to access them via CSJS.

So, I made the classic mistake of failing to know whether I and my data were client-side or server-side. Thus, my getComponent commands were getting a handle to the last version of the server-side component, not what the user just entered on the client-side. So, I needed to move back to the client-side to display client-side values. Fortunately, I remembered that if your client-side javascript evaluates to false, the server-side script never executes.

<xp:eventHandler event="onclick" submit="true" refreshMode="complete"
        immediate="false" save="true" id="eventHandler3">
        <xp:this.script><![CDATA[var baseText = "Are you sure that you want to set the exchange rate for ";
            var effectiveDate = document.getElementById("#{id:effectiveDate}").value;
            var localCurrency = document.getElementById("#{id:localCurrency}").value;
            var exchangeRate = document.getElementById("#{id:exchangeRate}").value;
            return window.confirm (baseText + localCurrency + " to " + exchangeRate + " as of " + effectiveDate + "?");]]>
        </xp:this.script>
        <xp:this.action><![CDATA[#{javascript:exchangeRateDoc.save();
    context.redirectToPage("/pro_exchangeRate_view.xsp")}]]>
        </xp:this.action>
</xp:eventHandler>

The key to the client-side javascript is to make sure you return the value of that window.confirm at the end. In my initial attempt, I didn’t return the value and my testers pointed out to me that my ‘Are you sure?’ was just taunting my users. It would ask the question, but it ignored the response. Clicking OK would save it, as intended, but clicking Cancel would ALSO save it! Talk about ignoring user input!

Hopefully, my mistake will prove instructive in your attempts to find your way in XPages…..

Categories: Client-Side Javascript, Server-Side Javascript | Tags: , , , , | 2 Comments

Simple dialog returning a value to your #XPages

Since I’m still new to XPages, I’m always finding things that are a challenge. Most of the time, I don’t know how big a challenge they are because doing things in XPages isn’t the same as doing them in Old Notes.

Business case

On the payment request in our procurement module, the user needs to enter the exchange rate between the local currency and the base currency for approval routing. Sometimes, the payment has already been made in local currency while the exchange rate has changed. They might only know the amounts in the two currencies, but didn’t record the exchange rate at the time.

PaymentRequest

Solution

In discussions, I suggested that it might not be difficult to simply have a popup that allowed the user to enter the amounts and return the value to the payment request in the UI.

Surprisingly to me, I was right. It’s not that hard. My challenge was that I decide to put this exchange rate computer into a custom control so that I’d be able to re-use it. That meant having to figure out how to have the dialog do a partial refresh on the payment request, in order to recompute the total amount in the base currency (USD).

Existing design considerations

Our procurement module was originally designed by the brilliant minds over at Teamwork Solutions, led by Scott Good. It’s very nice, but this wasn’t included in the original requirements gathering. (What percentage of actual requirements do get into the original requirements gathering?)

Each of those line items displayed in the payment request is a separate Notes document, so, in Old Notes terms, this would be like an embedded view, but since we’re in XPages, it’s a repeat. (I am learning to love repeats!) The total payable amounts are ‘computed fields’ in XPages, which is like a computed for display field in Old Notes. As such, they wouldn’t actually save anything to the payment request document. So, we have some non-displayed computed fields with some server-side Javsacript (SSJS) that aggregates all the local amounts from those view entries and puts it into the control on the XPage. As such, when the exchange rate changes, there is a partial refresh.

So, on our exchange rate control, we had a partial refresh for onchange. Simple control, bound to a field on our payment request document.

<xp:inputText id="exchangeRate" value="#{payDoc.Exchange_Rate}" style="text-align:right;width:70.0px;">
	<xp:this.converter>
		<xp:convertNumber type="number"></xp:convertNumber>
	</xp:this.converter>
	<xp:eventHandler event="onchange" submit="true" refreshMode="partial" refreshId="paymentCostInfoPanel">
	</xp:eventHandler>
</xp:inputText>

Design

So, I created my exchangeRateComputer custom control. I decided to make the link part of the custom control so that implementing it on any other XPage would require the least work possible.  I decided to always set the control on the XPage to be named “exchangeRate”, so I don’t have to pass a string with the control name. As an added bonus, I wanted to fill out the local currency field for the user and just let them enter the final base currency amount they’d like to see.

ExchangeRateCalculator

I ran into one problem. I couldn’t get it to do the partial refresh from the exchangeRateComputer custom control. I’d tried some XSP.partialRefreshGet commands but was having problems and realized…. I might want the name of the element to be refreshed to be different on different XPages. Since passing the compositeData value into the XSP.getElementById seemed beyond my capabilities, I decided to simply defer all refreshes to the exchangeRate control itself. So, I ended up adding an onblur partial refresh. When the user clicks on the ‘Apply to payment request button’, it puts focus onto the exchangeRate control, then, closes the dialog. Closing the dialog blurs focus from the exchangeRate control and…. with an added onblur event, performs a partial refresh for me. (The onblur event is identical to the onchange, except for the name.)

<?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:link escape="true" text="Compute Exchange Rate" id="computeLink" 
		style="margin-left:5px;">
		<xp:eventHandler event="onclick" submit="false">
			<xp:this.script><![CDATA[XSP.openDialog('#{id:computeDialog}');]]></xp:this.script>
		</xp:eventHandler>
	</xp:link>

	<xe:dialog id="computeDialog" title="Exchange rate calculator">
		<xp:table>
			<xp:tr>
				<xp:td>
				</xp:td>
				<xp:td>
					<xp:label id="instructions" value="Enter the amounts below to calculate an exchange rate">
					</xp:label>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td style="width:150px;text-align:right;">
					<xp:label id="total_baseCurrencyLabel" value="Total Base Currency">
					</xp:label>
				</xp:td>
				<xp:td>
					<xp:inputText id="total_baseCurrency" style="width:100px;text-align:right;">
						<xp:this.converter>
							<xp:convertNumber type="number"></xp:convertNumber>
						</xp:this.converter>
						<xp:eventHandler event="onchange" submit="true"
							refreshMode="complete">
							<xp:this.action><![CDATA[#{javascript:var base = getComponent("total_baseCurrency").getValue();
var local = getComponent("total_localCurrency").getValue();
rate = local / base;
getComponent("rate").setValue(rate);}]]></xp:this.action>
						</xp:eventHandler>
					</xp:inputText>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td style="text-align:right;">
					<xp:label id="total_localCurrencyLabel" value="Total Local Currency">
					</xp:label>
				</xp:td>
				<xp:td>
					<xp:inputText id="total_localCurrency"
						defaultValue="#{javascript:compositeData.localCurrency}"
						style="width:100px;text-align:right;">
						<xp:this.converter>
							<xp:convertNumber type="number"></xp:convertNumber>
						</xp:this.converter>
						<xp:this.validators>
							<xp:validateLongRange minimum="1"></xp:validateLongRange>
						</xp:this.validators>
						<xp:eventHandler event="onchange" submit="true"
							refreshMode="complete">
							<xp:this.action><![CDATA[#{javascript:var base = getComponent("total_baseCurrency").getValue();
var local = getComponent("total_localCurrency").getValue();
rate = local / base;
getComponent("rate").setValue(rate);}]]></xp:this.action>
						</xp:eventHandler>
					</xp:inputText>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td style="text-align:right;">
					<xp:label id="rateLabel" value="Exchange Rate"></xp:label>
				</xp:td>
				<xp:td>
					<xp:text escape="true" id="rate"></xp:text>
				</xp:td>
			</xp:tr>
			<xp:tr>
				<xp:td>
				</xp:td>
				<xp:td>
					<xp:button value="Apply to payment request"
						id="copyButton">
						<xp:eventHandler event="onclick" submit="true"
							refreshMode="complete">
							<xp:this.script><![CDATA[var rate = XSP.getElementById("#{id:rate}"); 
XSP.getElementById("#{id:exchangeRate}").value = rate.innerHTML;
XSP.getElementById("#{id:exchangeRate}").focus();
XSP.closeDialog('#{id:computeDialog}');]]></xp:this.script>
						</xp:eventHandler>
					</xp:button>
					<xp:button value="Cancel" id="cancelButton">
						<xp:eventHandler event="onclick"
							submit="false">
							<xp:this.script><![CDATA[XSP.closeDialog('#{id:computeDialog}');]]></xp:this.script>
						</xp:eventHandler>
					</xp:button>
				</xp:td>
			</xp:tr>
		</xp:table>
	</xe:dialog>
</xp:view>

The last tweak to it is that my computed exchange rate in the dialog is just a computed field. I struggled a little until a dogpile search revealed that I needed to get that value as innerHTML. If you try getValue() on a computed field, you get bupkis, but if you grab the innerHTML, you’ve got the world in your hands.

It feels like that would be easier in Old Notes, but I hardly care any more. I have a new micro-solution in my toolbox and I expect to re-use this not only for other exchange rate computations, but to re-use the dialog and value-passing in many places. It’s all about building up your toolkit, right?

Categories: Client-Side Javascript, Old Notes, Server-Side Javascript, Xpages, XSP Functions | Tags: , , , , , , , , , , | 1 Comment

Dirty pages and keeping users on them in #XPages

As I’ve mentioned before, sometimes XPages does not react the way your Notes users would expect it to react. In particular, I’ve had issues with ensuring that XPages warns the user when they try navigating away from a page that they’ve edited without first saving it. Such pages are “dirty”, since something has changed on them. Back in November we examined this in my post on Modified flags in XPages.

Unfortunately, sometimes I want to inform the user that they’re leaving a dirty page and other times I don’t. Also, sometimes, I seem to be able to add things that don’t warn the user that they’re leaving a dirty page. I’m sure there is consistency, but I haven’t figured out the rules, so I have added some code that handles these situations.

Simple warning of a dirty page

This was described in that prior post. Set the enableModifiedFlag to true and provide a couple of properties to handle it. This should fire whenever you close the XPage or navigate away via a standard link.

<xp:view xmlns:xp="http://www.ibm.com/xsp/core" enableModifiedFlag="true">
	<xp:this.modifiedControl>
		<![CDATA[#{javascript:if ( true ) {return "saveButton1"}}]]>
	</xp:this.modifiedControl>
	<xp:this.modifiedMessage>
		<![CDATA[#{javascript:"Purchase order modified. Click OK to save, cancel to continue without saving"}]]>
	</xp:this.modifiedMessage>
...
</xp:view>

Unfortunately, I’ve found that sometimes, it doesn’t warn me.

Warning when clicking links

I’ve got some navigation on the left side of my XPages, set up in a lovely dojo accordian with links to different parts of the application. However, not every link is set up in the same way. Some are simple and use the modifiedControl to warn users, like this one:

<xp:link text="Attachments" escape="true" id="link3" value="/att_attachmentManagement_view.xsp">
</xp:link>

Some, however, run some scripts to remove some sessionscope variables when switching pages and I was having challenges getting the server-side Javascript to execute. So, I found a way to do both:

<xp:link text="Payment requests" escape="true" id="paymentLink" target="_self" value="#">
	<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
		<xp:this.action><![CDATA[#{javascript:clearDynamicViewSettings();
context.redirectToPage("/pro_paymentRequest_view.xsp");}]]></xp:this.action>
	</xp:eventHandler>
</xp:link>

The unfortunate thing is that when I’m using the redirect, it ignores the dirtiness of the page. Fortunately, Per Henrik Lausten found a way to deal with it on StackOverflow (thanks to PSolano), and posted it as an XSnippet on OpenNTF.

if (XSP._isDirty()){
  if (confirm ("Are you sure you want to navigate away from this page?" + "\n" + "\n" +
    "This document may contain unsaved changes." + "\n" + "\n" +
    "Press OK to continue, or Cancel to stay on the current page.")) {
    return true;
  } else {
    return false;
  }
} else {
  return true;
}

So, when I added that explicitly as client-side Javascript to each eventhandler, it worked beautifully. Now, being a fan of reusable code, I wanted to put it in a CSJS script library and invoke the function all over the place. My function was as shown above, with a little wrapper:

function isClean() {
	if (XSP._isDirty()){
	    ....
	}
}

Then, I just needed to invoke it. My first attempt looked great to me. Call isClean() and the client side would warn the user, then stop them as appropriate.

<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
	<xp:this.action><![CDATA[#{javascript:clearDynamicViewSettings();
context.redirectToPage("/pro_paymentRequest_view.xsp");}]]></xp:this.action>
	<xp:this.script><![CDATA[isClean();]]></xp:this.script>
</xp:eventHandler>

Ummmm, but that didn’t work. In order to prevent the server-side Javascript from executing, the client-side Javascript needs to RETURN false, not just compute it.

<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
 <xp:this.action><![CDATA[#{javascript:clearDynamicViewSettings();
 context.redirectToPage("/pro_paymentRequest_view.xsp");}]]></xp:this.action>
 <xp:this.script><![CDATA[return isClean();]]></xp:this.script>
</xp:eventHandler>

That’s added bonus knowledge – not only are we learning how to stick to dirty pages, we’ve also learned that if you have the CSJS return a value of false, it will not execute the SSJS. I’m sure you can grasp the opportunities for validation provided there.

What about when you don’t want to warn them?

In our application, the user can select the action to perform from a combobox and then click a button to execute it. (Thanks to Scott Good, Henry Newberry and the folks at Teamwork Solutions!) The challenge here is that when the user changes the value in the combobox, the page is dirty! I mean, no data may have changed other than that combobox, but the XPage still feels dirty. Since some of those actions will result in saving the document and all of them result in navigation away from this page in an expected and managed way, we don’t want it to feel dirty. To dodge the problem, I just add a little CSJS to convince that page that it’s not really dirty.

<xp:this.script><![CDATA[XSP._setDirty(false,"");]]></xp:this.script>

So, then the modifiedControl is not tipped off and we execute the intended commands. Now, I’m pretty sure we don’t run into this problem with standard save buttons, but we’re in an unusual case of taking two steps to execute one action.

Note

YMMV. These private calls to XSP methods may go away and using them requires care to ensure you’ve got the right parameters, or so the XPages Portable Command Guide advises us. Of course, everything we code may call functions that will go away and we always need to get the parameters right. So, I’m gonna use them, though sparingly.

Categories: Client-Side Javascript, Server-Side Javascript, Xpages | Tags: , , , , , , , , , , , , | Leave a comment

Handling numeric ranges in text

I’ve often said in the last six months that the frustrating thing about XPages in comparison to “old Notes” is that sometimes things that were easy before, are now hard. So many of us have 12-18 years of Notes development and can code in LotusScript basically off the top of our heads that doing XPages is almost like starting over.

Such is the case I’ve recently had with handling some text keywords that determine dollar ranges for workflow routing. In the original XPages design, the dollar ranges were in a combo box, for the user to select manually. As such, all four range values were simply put as text values in a multi-value text field in a keyword document. Well, since asking (or trusting) one’s users to select the dollar range seems excessive when we could simply compute it for them, I decided to modify that part of the XPage I’d inherited.

Intially, the four values used in the combo box would return an alias value bound to a field on the document, so the keyword document had these values:

Under $500|1
$500 to $3,000|2
$3,001 to $150,000|3
Over $150,000|4

In my simple world of formula language and LotusScript, I would have been able to use a simple formula that included @TextToNumber, some @RightBack’s and @Left’s to establish what the range border values are. Tossing it into Client Side Javascript (CSJS), it vexed me. So, rather than handle it, I created a range keyword that just had the numeric values and a text keyword that had the nice display text. I promised myself I’d get back to it and in one of the miracles of the modern era, I got back to it within a few weeks (after Connect-o-Sphere both enthused and confused me in the same week).

There’s a computed field used to determine the total value of the purchase order, so I had snuck my code for determining the dollar range right into that formula. This intrigues me because I can have code to recompute the dollar range value only when that total value changes – an option I wouldn’t have had under “old Notes”, where it would have recomputed every time.

I realized that if I’m computing the text to display the dollar range anyway, I could drop the aliasing off the end of the keyword values, which should make them more accessible to my local admins for configuring their workflow. I used Javascript to bind the computed combo box value as follows:

var choices = new TS_Keywords().Lookup("Procurement / Checklist dollar ranges", true);
if (poDoc.isEditable()){
	var number = viewScope.dollarRange; }
else { number = poDoc.getItemValueInteger("DollarRange") };
if ( number > 0 ) {
	number = number-1;
}
return choices[number];

TS_Keywords was written for us by Teamwork Solutions, but basically we’re just getting the multi-value text field with it.

So, to handle that multi-value text field, I needed to use several String methods in Javscript: slice, length, replace and parseInt. I was rather surprised that it both looks like such a complex line of code, but is also relatively simple.

//compute dollarRange and store as a viewScope variable

var number = 4
// compute numeric values of the checklist ranges from the text keyword
var rawlevels = new TS_Keywords().Lookup("Procurement / Checklist dollar ranges", true);
var levels = new Array (rawlevels.length);
for (var i=0;i<rawlevels.length;i++) {
	var space = rawlevels[i].lastIndexOf("$");
	levels[i] = parseInt ( rawlevels[i].slice ( space + 1, rawlevels[i].length ).replace(",","") );
}

// find which range the total falls into
if ( thisVal <= levels[0] ) { number = 1 };
if ( thisVal > levels[0] && thisVal <= levels[1] ) { number = 2 }; 
if ( thisVal > levels[1] && thisVal <= levels[2] ) { number = 3 };
poDoc.setValue("DollarRange",@Integer(number));

While I’m sure this is not the most efficient way to handle this, it avoids the multiple keyword nightmare I was going to foist on those local administrators and might guide someone generally in the direction of solving their own coding problems.

Categories: Client-Side Javascript | Tags: , , , | Leave a comment

Create a free website or blog at WordPress.com.

%d bloggers like this: