Tuesday, March 04, 2008

Limiting Text Entry In The Lotus Notes Client

I got asked a question the other day that I didn't have a great answer to. A reader wanted to know of a way to limit the number of characters that could be typed into a field (e,g. restrict the user's entry to 30 characters). I don't remember having a business requirement like that in a Lotus Notes application for a long time, but in the past I'm sure I would have handled it with some form of input translation or validation formula using @Length. I'm vaguely aware of a script library I had at some point that probably included some code to do this, but it's long lost in the catacombs of old databases from past jobs. Regardless, I knew that any of those old solutions would be far from elegant in today's interface. Nope, what I needed was to whip up a fresh approach that presented a modern UI experience. Here are the characteristics I would want if I was implementing this feature:

-Immediate feedback about how many characters I have left
-Automatic trimming of my text entry once I hit the maximum number of characters
-Unobtrusive changes to the UI as I enter a length restricted field
-A flexible and simple way to implement the functionality multiple times on the same form.

I've seen many good examples of this in practice in web design before, so my first inclination was to use JavaScript. Of course, I'm not a huge fan of JS in the Lotus Notes client due to it's somewhat dubious support (at least in my experience), but I thought it should be able to handle this. Turns out that this is pretty easy to do. Here's a little snippet of the idea in action:


Click image for animated version


So to accomplish this, I first concentrated on putting together the JavaScript to handle the trimming and on-the-fly update of how many characters were remaining in the given field. On the web this would usually be accomplished with calls to onKeyDown and onKeyUp, but these are not exposed in the Notes object model. Instead, I leveraged the setInterval method of JavaScript. This method allows you to call a function over and over every x seconds. Using this concept, the function gets called as soon as the user enters the field and quits when they leave. Here's what I ended up with.

First, the code that does the checking. I tried to make it fairly generic.



The textLimiter function accepts the name of the field to be checked, the name of the field that serves as the counter and the maximum number of characters as arguments. In the If statement, we compare the number of characters (inputField.value.length) to the maximum allowed. If the string in the given field is longer than the max, we use the substring function to trim it to the appropriate length. If not, then we know we haven't reached the max length yet and the user can still type characters into the field. As they do so, the Else statement updates the value of the counter field by subtracting the length of the string in the given field from the maximum allowed number of characters. Pretty simple, no?

That's all that is housed in the JS Header of the Notes form. Now let's take a look at one of the fields that implements this code. For purposes of this example, I added the functionality to two fields. For each, I used the onFocus event of the field to initiate the call to the textLimiter function and used onBlur to clear the setInterval method. You can see how this appears in the "UserTitle" field below:


(Editor's Note: I used some creative editing to show the two events in the same screenshot)

As you can see in the onFocus event, I just set some variables for the given field then use setInterval to call the textLimiter function every 10ms. inputName is the name of the field you are limiting the characters in, counterName is the name of the field that is counting down the number of characters remaining and charLimit is the maximum number of characters allowed in the input field. The onBlur event includes the call to clearInterval, which means the client will stop invoking textLimiter as soon as we exit the field. (We'll skip the other stuff for now and come back to it later).

Notice in the screenshot above that I include a field to serve as the counter for each input field that I am checking. The display properties of this field are completely up to you. I chose to implement this field as an editable field with the field delimiters hidden. This allows you to update the value while giving the appearance of a computed for display field or a piece of computed text. By the way, I originally was going to use a hidden field as the counter and just place computed text where it was needed to refer to the hidden field. However, this would necessitate almost constant refreshing of the screen, which in my experience leads to a less than stellar UI performance. Keep in mind that however you plan to approach this, you need to have an editable text field somewhere so that your JavaScript code can write into it.

With just those few pieces in place, the functionality works quite nicely. As you type in the given field, the text underneath updates you with a message as to how many characters you have remaining. It's not enough to stop here, though. In order to keep the screen as streamlined as possible, I don't want any user input messages to be displayed except for when I need them. This speaks to the third point in my list of requirements. What I want to happen is for the helper text to appear as soon as I enter the field and disappear as soon as I leave it. That's where the rest of the code comes in.

I chose to implement the dynamic nature of this functionality with simple hide-when formulas. I placed the helper text on a separate line, then set it's hide when formula to hidden except when the "CheckField" counter was equal to the name of the counter's corresponding input field.



To trigger the display of the counter when the user enters the field, it is necessary to tell Notes what line we should be showing and then perform a refresh of the document (or the less expensive RefreshHideFormulas if you're not recalculating anything). That's where a little hack comes into play. See the button at the top of the form? This button has a single line of code - @Command([ViewRefreshFields]). You can trigger this via JavaScript in order to call a refresh of the document. Thus, in our example, when the user clicks into the UserTitle field, the following two lines work to display the counter:

f.CheckField.value = 'UserTitle';
f.RefreshItButton.click();

The first line sets the value of "CheckField" to the name of the input field that corresponds to this counter and the second line "clicks" the hidden button, refreshing the doc and showing the counter line. When the user exits the field, we just do the reverse, setting "CheckField" to null and refreshing the document again to hide the counter.

When you put all this stuff together, you get a nice little bit of functionality. It accomplishes the goal of restricting user input and does so in a fairly elegant way. You also get a handy little example database where you can try this out yourself.

Now, since that's all out of the way, I'd like to hear how you would improve this. As I said, my experience with JavaScript in the client is pretty minimal and I may have missed some obvious places to make this easier. This was a pretty quick proof of concept and I'm sure there is room to make it better. For example, the "hack" to refresh the document via JavaScript is a pretty old one, but I've not come across any other solutions for it in recent years. If you have some ideas to make this functionality better, please share with the rest of the class by including a comment.

Addendum: Well, what do you know. Had I not already done the work and typed up half of this post, I would have just quit this as it turns out that Mr. Robichaux has done something very similar. Our approaches were quite alike, but I think this one has at least one advantage in that it keeps the field trimmed at the set number of characters. In any case, if you'd like to see how he tackled the problem, check it out here.

8 comments:

Thilo Hamberger said...

Hi Chris, isn't that the same idea as "Press 'Enter' In A Field And Trigger An Action" with the same old problems? Did you test it with several forms open?

Michael Schlömp said...

It's a very smart solution and I tried it myself. Erverything works fine, but I try to set the cursor with @Command([EditGotoField]) into my checked field. If you use these @Command the "ON FOCUS" Event is not properly entered and press the hidden button did not work.
Any idea for this.

Michael

tbahn said...

Hi Chris,

my colleage - Bernd Hort - did something VERY similar last October:
http://www.assono.de/blog/d6plinks/BHOT-77NDHM

tekipaq said...

Hi Chris,

I really like this implementation, as it is exactly what we needed.

However, we are getting error messages in the client if windows are left open with the Limiting text feature on the form.

These JS errors occur when going into other documents and occur after about 30 - 40 minutes.

1. Javascript Out of Memory Error
2. Javascript Cannot Compile Error

The client needs to be closed are reopened. Do you know what might be causing these JS errors????

Thanks much,

Tekipaq

Chris Blatnick said...

@tekipaq...As far as I know, it's just a memory leak with the JavaScript in the client. I don't know that there is much you can do to prevent it other than make sure those windows are closed. Sorry I can't provide much wisdom here. I know it was a problem with R6 and 7...but I have not tested it in 8. Good luck!

Oni said...

> These JS errors occur when
> going into other documents and > occur after about 30 - 40
> minutes.
> 1. Javascript Out of Memory Error

=================================

There is some bug, to avoid this on the form event "onUnload" write this (Client ~ JavaScript):

if (useClick > 0) {
clearInterval(useClick);
}

=================================

Expl.: Becose when you make focus on the field, and later, for ex., press some Button (save or other), the event "onBlur" didnt occur and the funcion textLimiter(..) is still working..

larryg said...

In UserTitle onBlur what is the purpose of the last line, "f.UserSalary.focus()"?

There is no corresponding entry in the UserSalary onBlur. Why?

Thank you.
NotAJavascriptProgrammer

Ked said...

This really is a great technique, it works seamlessly in dialog boxes too.

However, there is one issue I can't resolve:

If you want to use this code on a field that has default focus, then the OnFocus event isn't triggered and the code doesn't kick in.

This seems similar to Michael's earlier issue.

Any ideas on how to deal with default focus?