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:
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:
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 IntegerSet uidoc = ws.CurrentDocument
intCounter = 0While intCounter < 20
Call uidoc.FieldSetText(“NameSuggestion_” + Cstr(intCounter), “”)
intCounter = intCounter + 1
WendCall 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):
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_15 – 19 will be hidden and 0 – 14 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:
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:
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:
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:
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:
Make sure you set that the JavaScript should run inside the 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 Velgvar 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:
I set the button to 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:
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 ERRORHANDLERDim 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 IntegerSet 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 IfCall uidocThis.Refresh
DONE:
Exit SubERRORHANDLER:
MsgBox “Feil: ” & Chr$( 13 ) & “Feiltype: ” & Err & “. Feilmelding: ” + Error$ + ” i linje ” & Erl & “!” , , “BEKLAGER PROGRAMFEIL”
GoTo DONEEnd 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
WendEnd 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 EnhetsregisteretWe 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 StringSet 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 IfCall uidocThis.FieldSetText(“iPage”, docForetak.Url(0))
Call uidocThis.Refresh
End IfDONE:
Exit SubEnd 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!