Typeahead Component

I have written a component allowing easy implementation of typeahead functions for variables or fields.

The idea is to create an array with possible values (“suggestions”) and to place a method (the only shared method of the component) in the on after edit event of the variable or field. To this method, you pass the object name of the variable or field and a pointer to the array with the values.

As soon as the user starts to type, the component displays a typeahead dialog, a bit similar as the one we 4D programmers are used to work with when writing code in the 4D method editor.

The component is free and open source, currently 4D v14 based but should be easily portable to newer versions.

Download link:

https://www.asuswebstorage.com/navigate/s/E382754C0DCE455CB56D3BA121D8C474W

Best,

Olivier Flury

Enfin une solution de saisie assistée avec une liste venue d’ailleurs !

:star::star::star::star:
… ou les emoji au secours de l’absence de vote en zone de partage de code. :mrgreen:

Hi Olivier,

Again thank you for your generosity.

Quick question. In the test the array of values is preset in the On Load event. In my case the searches are always dynamic and only results are displayed.

So I am guessing I just need to move the code into the “On After Edit” event and:

do the searching

fill the “atpu_Values” array

Then call …

Typeahead (OBJECT Get name(Object current);Get edited text;$p2atpu_Values)

Is that about right?

Appreciate,
John…

The idea is to search in an array and not in records. That is much faster. Prepare your array (in advance) and pass it to the component in the on after edit event. The component will search in the array and present the match(es) in the listbox.

In the example we prepare an array with the names of all countries. Consider this as a kind of “selection where to search in”.

The example delivered with the component also shows that we could pass the jocker (@) in the on getting focus event to immediately show the user the list of all values. As soon as he starts typing, the list (a copy of the array) is adapted accordingly to the matches. But this is just an option, I guess, that most situations require to react on the on after edit event only.

e.g. the user starts to type and enters “s”, the list shows all matches starting with “s” = countries starting with “s”. If there is no match with “starts with”, the component searches “contains”. This allows us for example to find my country (Switzerland) by typing “witz”. :wink:

http://dict.leo.org/englisch-deutsch/Witz

Hi Olivier,

Ref: “The idea is to search in an array and not in records.”

Yes, I could store the names I need in the array and then allow the routine to do it’s magic. I haven;t looked closely enough yet BUT…

Does it work with parallel arrays so that I can match to an ID so I can associate with a record ID or some other unique ID?

Ref: “e.g. the user starts to type and enters “s”, the list shows all matches starting with “s” = countries starting with “s”. If there is no match with “starts with”, the component searches “contains”…”

In the current set of scenarios I will require a “contains” as the characters are typed. So since the routine handles that I don’t need to worry about it.

Thanks for the explanation. Very helpful.

Appreciate,
John…

Hi John,

Regarding “Does it work with parallel arrays so that I can match to an ID so I can associate with a record ID or some other unique ID?”

My idea when I was creating this component was to have an alternative for popups and combo boxes. So we are at the level of a field or variable and not at the level of an entire record. Popups and combo boxes should contain a list of distinct values. For the typeahead component applies the same idea: you should pass an array with distinct values. If you follow this principle, there is no need to pass an array with ID’s to the component and to receive back an ID. You lookup the value the user selected and search it in your array. From this point you can easely map the value to an ID.

Keep in mind that the component allows the user to enter a value, similar as with a combo box.

For selecting an entire record, I use an other component to which you can pass several arrays. It displays a window with a search field and a listbox with the possible matches. It returns the ID of the selected row. If interested, I could publish this component as well.

Best,

Olivier

Hi Olivier,

So in most cases this would be perfect. But in my case there will be non-unique values and I still need them to show up.

Ref: “For selecting an entire record, I use an other component to which you can pass several arrays. It displays a window with a search field and a listbox with the possible matches. It returns the ID of the selected row. If interested, I could publish this component as well.”

I would be interested! As that’s exactly what I need.

Appreciate your generosity,
John…

I have something like that for a long time and implemented two ideas for improvement :

  1. the array may NOT be sorted. In the picture below, entries are listed by frequencies of occurences in the database (‘Marc’ is more present than ‘Martine’ and much more than ‘Margaret’ in the bottom )

  2. in your component if you type ‘ma’, you get my list for example, but if you add the letter ‘r’ to get ‘mar’ you are still stuck with the entry Marc that still matches the letters typed. But if you had to add ‘r’, that means that you are NOT interested in ‘Marc’, so why not advance to the next compatible entry, that is ‘Martine’

Over the type, this is terribly efficient to enter values because you do not have to look for the down arrow or the mouse to point the entry you need.

To do that I keep in memory the index of the value suggested and search in the array from the next value on the next pass.

[]19083830;“typeAhead”[/]

Thanks for the input, good ideas.

Regarding point 1: The typeahead component expects an array with possible values, so you could sort the values not from A-Z but e.g. by relevance. The component starts the search from top.

Regarding point 2: Many user type very fast and get quickly a short list of possibilities. Then they move by tab to the item they want and select it by enter. No need to move the hands away from the keyboard (or to use the down arrow key).

IMHO, users like obvious interface elements. And a sorted list of elements from which to select one seems obvious to me.

Hi John,

Here is the link to download the “record selector” component:

https://www.asuswebstorage.com/navigate/s/AA84929E41604CA4811013EB17D58FE0W

Let me know if you have any questions.

Best,

Olivier

Thanks again, Olivier :slight_smile:

I have something similar but I use a selection listbox, so I’m wondering why an array one: do you wish the content to be filtered already by the caller?

:star::star::star::star:

oops, I forgot the vote… :mrgreen:

I have started to look over the code, extremely well written and efficient and I suggest 2 things :

  1. if you type and after a few letters, you have no more entries compatible but the popup window of suggestions remains in place empty. To change that i added 3 lines in the method QueryFieldEventHandler at lines 88-93 :

[]19085401;“QueryFieldEventHandler”[/]

  1. if you have a unique entry compatible, it is better to validate by tab rather than to do it twice (by going to the unique entry in the pop up and back. For that I changed the method in the trap tab button :

[]19085413;“TrapTab”[/]

… now i hope to add the feature I suggested before but that need time…

Also when I fully delete ALL the letters of my input and if I validate, the first letter appears again …

so I changed in typahead to accept tSelected="" as :

[]19085494;“Typeahead”[/]

Regarding the record selector component? [Maybe we schould have opened a separate thread]. I prefer arrays because they are more flexible and we do not have to query a selection but arrays. And this is faster, depending on the data structure even much faster.

The content (= “selection” to display and where to query into = arrays filled with selection to array) is in my experience most of the time a subset of a selection of records. Imagine you have an [Order] table and a detail form where to add new orders. You will probably display to the user only a subset of the [Client] records: exclude the ones that do no longer exist etc.

Thanks Hervé!

I implemented your changes, the current version is available here:

https://www.asuswebstorage.com/navigate/s/B7C6589918CD438E95737E069CB5A0D4W

I made some changes on my side as well: I added a 4th (optional) parameter to the Typeahead method, now you can set the height of the window. And I modified some of the gui elements in order to display correctly on very short variables or fields.

Olivier

Hi Olivier,

Thank you again for your generosity.

And an interesting discussion based upon how others need to work with typeahead.

Much appreciated,
John…

bon, j’ai réussi à implémenter mes propositions itératives, mais là avec le renommage des variables, je donne la méthode un peu chamboulée (j’ai enlevé la possibilité des 2 jokers @saisie@)

<code 4D>
// QueryFieldEventHandler

// User name (OS): Olivier Flury - FLURY SOFTWARE
// Date and time: 17.02.17, 16:08:39
// ----------------------------------------------------
// Method: QueryFieldEventHandler
// Description
// Event handler of the variable tQuery
//
// Parameters:
// $1, text, object name of the variable
// $2, text, event
// $3, text, optional (edited text)

C_TEXTE($1;$2;$3;$tObject;$tEvent;$saisie;$tTypeahead;$proposition)
C_ENTIER LONG($indice;$finMotSaisie)

$tObject:=$1
$tEvent:=$2
$saisie:=$3

Au cas ou

: ($tEvent=“OnLoad”)

OBJET FIXER POLICE(;$tObject+"@";tFontName)
OBJET FIXER TAILLE POLICE(
;$tObject+"@";lFontSize)
OBJET FIXER STYLE POLICE(;$tObject+"@";lFontStyle)
OBJET FIXER ALIGNEMENT HORIZONTAL(
;$tObject;lAlignment)
OBJET FIXER COULEURS RVB(*;$tObject;lForeGroundColor;lBackGroundColor)

$finMotSaisie:=Longueur(tQuery)+1

Si ((“a”=tQuery) & (“b”=tQuery)) // On a entré l’arobase joker
SÉLECTIONNER TEXTE(tQuery;1;$finMotSaisie) // on sélectionne toute l’arobase
Sinon
SÉLECTIONNER TEXTE(tQuery;$finMotSaisie;$finMotSaisie) // set the cursor at the end
Fin de si

DerniereProposition:=""

: ($tEvent=“OnAfterEdit”)

Si ($saisie="") // on a tout effacé

EFFACER VARIABLE(tQueryTypeahead)
DerniereProposition:=""
tSelected:=""
VALIDER

Sinon

EFFACER VARIABLE(ValeursAffichees)
$saisie:=Uty_AddJockerToString ($saisie)

$indice:=Chercher dans tableau(ValeursPossibles;$saisie)

Si ($indice=Aucun enregistrement courant) // chou blanc, on efface la fenêtre des choix

EFFACER VARIABLE(tQueryTypeahead)
DerniereProposition:=""
tSelected:=$3
VALIDER

Sinon // au moins un choix possible

$proposition:=ValeursPossibles{$indice}

// Recherche et construction du tablau des nouvelles valeurs compatibles :

$indice:=1

Repeter
$indice:=Chercher dans tableau(ValeursPossibles;$saisie;$indice)

Si ($indice#Aucun enregistrement courant)
AJOUTER À TABLEAU(ValeursAffichees;ValeursPossibles{$indice})
$indice:=$indice+1
Fin de si
Jusque ($indice=Aucun enregistrement courant)

// Affichage d’une proposition, nouvelle si possible

$indice:=Chercher dans tableau(ValeursAffichees;DerniereProposition)

Si ($indice#Aucun enregistrement courant) // la précèdente proposition est toujours d’actualité

$indice:=Chercher dans tableau(ValeursAffichees;$saisie;$indice+1) // on regarde alors une autre proposition marcherait aussi

Si ($indice#Aucun enregistrement courant)
$proposition:=ValeursAffichees{$indice} // c’est le cas, on va donc la suggérer en lieu et place de l’ancienne
Fin de si
Fin de si

$tTypeahead:=Sous chaîne($saisie;1;Longueur($saisie)-1)
tQueryTypeahead:=$tTypeahead+Sous chaîne($proposition;Longueur($tTypeahead)+1) // pour respecter la casse des lettres frappées
DerniereProposition:=$proposition // on mémorise cette dernière proposition
Fin de si
Fin de si

: ($tEvent=“DownArrowKey”)

ALLER À OBJET(*;“ablb_Typahead”)
MainEventHandler (“ablb_Typahead”;“SelectFirst”)
Fin de cas

</code 4D>

et pour valider directement la proposition, j’ai du simplifier le bouton TrapTab pour valider aussitôt avec la touche tab

[]19085603;“Your comment here…”[/]

bon, il est minuit en Thailande… bonsoir

: Olivier FLURY

[Maybe we schould have opened a separate thread]
Yes, but the brake on my keyboard was broken :mrgreen:

Last details :

I noticed that when you navigate through the suggestions with Tabulation key, you go back to the input field when you reach the bottom of the list. I prefer to go back to the top of the list with that code :

[]19089019;“FirstLine”[/]

Then this component solved a problem I had with users for YEARS : sometimes you need to change the case of an entry that somebody added by error, or you want to change the case (of the second firstname for example) as in :

[]19089020;“accent”[/]

[]19089022;“case”[/]

As a side effect I noticed that the tabulation key takes the suggestion, but the return key keeps intact what you typed, therefore a tip was necessary to explain that but :

  • tip are not working in pop-up window
  • tip was not displayed in the shadow entry
    but luckily, it worked in the tQuery variable :

[]19089024;“tip”[/]

everything is working fine now. Thanks a lot

and the subcomponent “FSUty” contains so many utilities methods, worth to have a look at it indeed !!