How to Get Typeahead in a Standard Notes Text Field

Typeahead in standard Notes text field

I was challenged by my boss to come up with a solution to a problem that one of our customers wanted solved. Could I create typeahead in a standard Notes text field? This is how I did it.

You do have the possibility for typahead in the Notes client, but not in a standard text field. You can do it a Names field, in a pull down or dialogue list and in some other instances. However, these are a bit limited, especially when it comes to suggestions for values. The exception here is the names field in emails. However, a standard text field has no such functionality.

PS! Short demo video at the end of the blog posting!

The challenge

In Norway we have a public database (BRREG) listing all companies and organisations. This archive is searchable, and it has it’s own API you can use to fetch data from it. Our customers use our iBricks product, which includes a database of their customers (CRM), where they can store communications with contacts within organisations and companies.

When our customer wanted to register a new company in their CRM, he was tired of having to manually look up the spelling in the public BRREG-database, as well as going there to find the address, organisation number and so on. So he asked if we could create the following functionality:

Whenever he started typing in the company name field, it would start searching for a company starting with the characters he had typed, and show a list of the names. He should then be able to click on the correct name from the list of company names. When he did so, all data about that company should automatically be filled out in the fields in the form.

Our own local copy

The first thing we did was that my boss created a Notes application where we stored data about all the companies in the public BRREG-database. He used a Java agent and the public database’s REST API to fetch the data and then the Java agent saved this in our local database called Enhetsregisteret.

Once a day, an agent uses the REST API to find any updated data in the public database. Any organisation no longer in the public database will be deleted from our application, any new company will be added and any updated data about existing organisation will be updated in our application as well.

Our application has several views, and I will be using the view that shows all organisations sorted by name in the typeahead solution.

The form

The Name field

The form for registering a new organisation has a field called Name:

Name field

When you set focus on this field by placing the cursor there, it will trigger a search based on the characters you are typing. The search will trigger every 500th millisecond and update the typeahead display. In the sections below, I will explain exactly what happens.

Default value

In the Default Value property of the field I have the following value:

REM {As long as this field has focus, it will contiously call on the JavaScript function searchName in JSHeader. When the field loses focus, it will stop triggering this function};
“”

onBlur event

In the onBlur event I have the following code:

//When this field is exited, it will stop triggering the function searchName in JSHeader
stopListener();

This is calling a JavaScript function that I will soon show you. It means that as soon as you leave this field, and it no longer has focus, it will call the function stopListener()

PS! You must make sure you have chosen Run: Client and JavaScript in the pull down fields above the formula field:

onBlur

 

onFocus event

In the onFocus event I have the following code:

//When we enter this field, it will start triggering the function searchName in JSHeader every 500 ms
startListener(this);

This is calling a JavaScript function called startListener(this) that I will also show you soon. It sends this field as a variable into the function.

As with the onBlur event, you have to makes sure that it’s set to run JavaScript in the client.

Exiting event

In the Exiting event I have the following LotusScript code:

Sub Exiting( Source As Field )
Dim ws As New NotesUIWorkspace
Dim uidoc As NotesUIDocument
Dim intCounter As Integer

Set uidoc = ws.CurrentDocument
intCounter = 0

While intCounter < 20
Call uidoc.FieldSetText(“NameSuggestion_” + Cstr(intCounter), “”)
intCounter = intCounter + 1
Wend

Call uidoc.Refresh
End Sub

Now, what is happening here? I have 20 fields called NameSuggestion_0 – 19. These will contain suggestion for names during the typeahead. When I exit the Name field, the searching for name suggestions will stop, but the suggested names will still be displayed in these fields. This code makes sure that all those fields are empty. This will then hide the fields.

Read on for an explanation about thee NameSuggestion fields.

NameSuggestion fields

Under the Name field I have twenty important fields called NameSuggestion_x (where x goes from 0 to 19):

Name suggestion fields

These are the fields that simulates typeahead. These fields contains the nth (nth being the number of each given field) found company name, based on the search done with the value typed into the field Name. They continuously updated as long as the user has the cursors inside the Name field.

So when the search is triggered, it will take the 20 first hits and put one company name into each NameSuggestion field. It will also take the Unique Document Id for each company document and put them into twenty hidden fields called suggestedDocID (see further down for a description of these fields).

There is also a field under all the NameSuggestion fields called antallTreff that will show you how many hits you have with the current search.

If there are less than 20 hits, the fields not containing a value will be hidden. So if you have 15 hits, the NameSuggestion_1519 will be hidden and 014 will be displayed.

All these fields are computed text fields, and they are all hotspots, which makes them clickable.

Value

The default value of the fields are:

REM {This field contains the last found company name, based on the search done with the value typed into the field Name. It’s continously updated as long as the user is typing characters into Name};
NameSuggestion_0

This is for the field NameSuggestion_0. The default value for all the other fields will be the same, only the 0 will be replaced by that field’s number.

Hide When

The Hide When properties look like this:

Hide When Properties

The field name in Hide Paragraph if formula is true formula must of course be the same as the name of  this field. Looking at the formula above, we know we are in the field NameSuggestion_11. You get the general idea…

The fields will be hidden when they are empty. That is why we in the Exiting code in the Name field make sure that all these fields don’t have values. That way, they will not be displayed when you are not typing in the Name field.

Hotspot

The hotspot covers the entire field and is an Action hotspot The setting looks like this:

Hotspot

The code is LotusScript:

Call chooseNameSuggestion(“11”)

The function chooseNameSuggested(“11”) is defined in a script library, but you can easily put it in the (Globals) section of the form itself. The function takes a text version of the field number as it’s argument, and I will show you the function further down

All the NameSuggestion fields have the same code in their respective hotspots, only with their own number as the function argument, of course.

antallTreff

This is a counting field (antall treff means number of hits in Norwegian) which displays the number of actual hits you have for the current search. It’s updated while you are typing. It will be displayed under the NameSuggestion fields.

The field is a computed field with the default value antallTreff. The hide-when formula is this:

Hide When

suggestedDocID

In addition to fetching the names of the companies found while typing, and putting them in the NamesSuggestion fields, we also get the corresponding Unique Document ID for each company document we get the names from. They are stored in 20 hidden fields called suggestedDocId_0 – 19 . I’ve hidden them under their own section at the top of the form:

Suggested Doc IDS

They are all computed text fields with their own name as the default value. The entire section is set to be hidden all the time.

JS Header

You will find the form’s JS Header section here:

JS HEader

 

Make sure you set that the JavaScript should run inside the client:

Run JavaScript in client

 

Now, running JavaScript inside the Notes client can get ugly. However, I’ve so far not had any trouble with this typeahead function, and none of my customers have complained either. So let’s live a little dangerously, eh?

Here is the code I’ve put in JS Header, with comments

//The code here is used to trigger a hidden hotspot button that will trigger a Lotusscript function that will search the application Enhetsregisteret for any company containing the characters typed into the field Name
//The Lotusscript function will put the result into the field NameSuggestion, where it can be chosen by clicking on the hotspot button Velg

var listenField; // the field object we’re currently listening on
var f = document.forms[0]; //this form

//this will start the search function, which will run as long as the field Name is in focus, every 500 ms
function startListener (field) {
listenField = field;
searchForName();
}

//this stops the search function when the field Name is exited (loses focus)
function stopListener () {
listenField = null;
}

//this function triggers the hidden hotspot button. The button will then search Enhetsregisteret for a company matching the characters typed into the field Name and put the first search result
//into the field NameSuggestion
function searchForName () {
// if there is no field to listen on, just exit
if (listenField == null)
return false;

//this is where we trigger the hidden button
f.refreshItButton.click();
// check again in 500 milliseconds
setTimeout(“searchForName()”, 500);

return true;
}

Global variables

I declare two variables:

listenField is basically just the Name field where you are typing to trigger the search

f  is basically this form. I used the ancient JavaScript code document.forms[0] to set this. I can now use f to get access to objects in the form via their HTML Name. I will use it to trigger a hotspot button.

startListener

What basically happens is that in the moment you put the cursor inside the Name field, it will call the function startListener. This function triggers the function searchForName

stopListener

The moment you leave the Name field, this function is called. It will reset the variable listenerField and stop the whole typeahead process.

searchForName()

This function is called by startListener.  It will trigger a hidden hotspot button (see below) and then it will recursively call itself every 500th millisecond, so that the hotspot button is triggered continuously. The hidden hotspot button is what triggers the search itself.

The code that triggers the button is this: f.refreshItButton.click(); The button’s Name property must therefore be refreshItButton (read on for a description).

Hidden hotspot button

Now, this is where the real trick happens.

This is a button that is hidden at the top of my form, and I recommend that this is where you put it as well:

Hotspot button

 

I set the button to run LotusScript:

Run LotusScript

 

I have simply one line of code in the Click event:

Call searchEnhetsregister()

This basically calls a function from the script library I use, you can easily put it in the (Globals) section of the form itself.

As described above, it’s the searchForName() function that calls this button via the JavaSCript code f.refreshItButton.click(); For the function to be able to do this, we must set the HTML Name to refreshItButton in the buttons properties:

HTML Name

 

LotusScript functions

Here are the LotusScript functions you will need. As I’ve mentioned, they are in a script library.

searchEnhetsregister()

The function that does the searching, namely searchEnhetsregister(),  is triggered from the button described in the section directly above. The function looks like this:

%REM
Sub searchEnhetsregister
Description: This funcion is triggered by the function searchForName () in the JSHeader of the Name form.
When a user has entered 3 characters or more in the field Name, the code
in this button will search in the Notes application Enhetsregisteret to find the first
documents that matches these characters. It will get the full name of the companies and
put them into the SuggestedName fields. It will also fetch the documents UniversalID and
put it in the hidden fields suggestedDocId.

The button is triggered every 500 milisecond so that if a user continues typing, the function will
do a new search for each character that is typed.
%END REM
Sub searchEnhetsregister()
On Error GoTo ERRORHANDLER

Dim s As New NotesSession
Dim ws As New NotesUIWorkspace
Dim dbForetak As NotesDatabase ‘Enhetsregisteret
Dim dcForetak As NotesDocumentCollection ‘the search result
Dim docForetak As NotesDocument ‘first document from the search result
Dim uidocThis As NotesUIDocument ‘this document
Dim strSoekeNavn As String, strQuery As String
Dim intCounter As Integer, intNumberOfDocs As Integer, intNumberOfFields As Integer

Set dbForetak = s.GetDatabase(“App01”, “all\foretakreg.nsf”, False)

If dbForetak Is Nothing Then GoTo DONE

Set uidocThis = ws.CurrentDocument
intCounter = 0
‘We get the characters from the field Name
strSoekeNavn = uidocThis.FieldGetText(“Name”)

‘We will only start searching if the user has typed at least 3 characters
If Len(strSoekeNavn) < 3 Then GoTo DONE

‘We create the search query and do the search
strQuery = {[Form] = “Foretak” & } & {[Name] = “} & strSoekeNavn & {*”}
Set dcForetak = dbForetak.FTSearch(strQuery, 0)

‘If the search result is empty, we exit
If dcForetak.Count < 1 Then GoTo DONE

‘If the search result has more than 20 docs, we still set it to 20 docs, as that is the maximum number that will be displayed
If dcForetak.Count > 20 Then
intNumberOfDocs = 20
Else
intNumberOfDocs = dcForetak.Count
End If

‘We fetch fhe first document found in the search result
Set docForetak = dcForetak.Getfirstdocument()

‘We add the companies name to the NameSuggestion fields and add the company document’s UniversalID to the hidden fields suggestedDocID
Do While intCounter < intNumberOfDocs
Call uidocThis.FieldSetText(“NameSuggestion_” & CStr(intCounter), docForetak.Name(0))
Call uidocThis.FieldSetText(“suggestedDocID_” & CStr(intCounter), docForetak.UniversalID)
intCounter = intCounter + 1
Set docForetak = dcForetak.GetNextDocument(docForetak)
Loop

‘We set the total number of documents returned by the search result
Call uidocThis.FieldSetText(“antallTreff”, CStr(dcForetak.Count))

‘If the search result has less than 10 hits, we need to empty the NameSuggestionFields not needed
If intNumberofDocs < 20 Then
Call emptyFields(intNumberOfDocs, uidocThis)
End If

Call uidocThis.Refresh
DONE:
Exit Sub

ERRORHANDLER:
MsgBox “Feil: ” & Chr$( 13 ) & “Feiltype: ” & Err & “. Feilmelding: ” + Error$ + ” i linje ” & Erl & “!” , , “BEKLAGER PROGRAMFEIL”
GoTo DONE

End Sub

emptyFields(intCount As Integer, uidoc As NotesUIDocument)

In the function searchEnhetsregister() we call a function called emptyFields. This function clears the NameSuggestion and suggestedDocIdD fields of any values. It looks like this:

Sub emptyFields(intCount As Integer, uidoc As NotesUIDocument)

While intCount < 20
Call uidoc.FieldSetText(“NameSuggestion_” & CStr(intCount), “”)
Call uidoc.FieldSetText(“suggestedDocID_” & CStr(intCount), “”)

intCount = intCount + 1
Wend

End Sub

chooseNameSuggested

When a user clicks on a name from the typeaheadlist, what she does is basically click on a hotspot action over a computed field. The hotspot calls the LotusScript function that fetches this company and imports its data into this form:

%REM
Sub chooseNameSuggested
Description: This function fetches the UniversalID that is stored in the hidden field suggestedDocID to fetch data from that document in Enhetsregisteret

We then populate the fields listed below with data from the document.

%END REM
Sub chooseNameSuggested(strFieldNumber As String)
Dim s As New NotesSession
Dim ws As New NotesUIWorkspace
Dim dbForetak As NotesDatabase ‘the application Enhetsregisteret
Dim uidocThis As NotesUIDocument ‘this document
Dim docForetak As NotesDocument ‘the document with the given UniversalID in Enhetsregisteret
Dim sText As String

Set dbForetak = s.GetDatabase(“[SERVERNAME]”, “[FILEPATH]”, False)

If dbForetak Is Nothing Then GoTo DONE

Set uidocThis = ws.CurrentDocument

‘We fetch the document in Enhetsregisteret
Set docForetak = dbForetak.GetDocumentByUNID(uidocThis.FieldGetText(“suggestedDocID_” & strFieldNumber))

‘If the document is found, we populate the fields in this form with the data from that document
If Not docForetak Is Nothing Then
Call uidocThis.FieldSetText(“Name”, docForetak.Name(0))
Call emptyFields(0, uidocThis)
Call uidocThis.FieldSetText(“compFnum”, docForetak.OrgNumber(0))
Call uidocThis.FieldSetText(“adrPost”, docForetak.BaAddress(0))
Call uidocThis.FieldSetText(“adrZip”, docForetak.BaZip(0))
Call uidocThis.FieldSetText(“adrStreet”, docForetak.PaAddress(0))
Call uidocThis.FieldSetText(“adrVisitZip”, docForetak.PaZip(0))

If docForetak.PhoneNumber(0) <> “” Then
Call uidocThis.FieldSetText(“Phone1”, docForetak.PhoneNumber(0))
Else
Call uidocThis.FieldSetText(“Phone1”, docForetak.CellPhoneNumber(0))
End If

Call uidocThis.FieldSetText(“iPage”, docForetak.Url(0))
Call uidocThis.Refresh
End If

DONE:
Exit Sub

End Sub

Phew! I hope I explained this in a good way, so that you can understand it. Huge thanks to Chris Blatnick and Julian Robichaux whose old blogs I plundered for tips when it comes to triggering a hotspot button via JavaScript.

Demo

Here’s what it looks like in action:

 

What do you think? Lots of work for a small functionality, but it was fun, and the users love it!

13 thoughts on “How to Get Typeahead in a Standard Notes Text Field”

  1. Cool stuff Hogne.
    Maybe you can even simplify your formulas if the field name is not hard-coded by using @thisname. Just an idea!

    1. Thanks, Thilo! Yeah, I actually thought about that when I wrote the blog posting. “Why didn’t I just use @thisname here?” 🙂

  2. Apparently @thisname will evaluate to “” in the hide paragraph field. You have to use the actual field name.

  3. I thought I was the only one doing such crazy stuff with js.. Hehe.. Glad to know I’m not alone.

    May I suggest that Sub searchEnhetsregister() can have some enhancement by storing the last search query in a Static variable, and compare it first thing before doing any other work.
    The comment in the Sub says that it only does the lookup if there’s been a change, but I didn’t see that in the code.
    You can also keep the lookup DB and View objects in Static/Global variables to not require obtaining them on every run.

Leave a Reply to Oliver Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.