3

I'm developing a autocomplete solution with a database search based on typing events (onkeyup) that the user inputs in a <h:inputText />.

The problem is that i have two pre-conditions that the application must attend to do a server-side search:

  1. The text informed is >= 3 and <= 6 characters
  2. User stopped typing for a few miliseconds (Preventing overload the server with to much search)

Without the item 2) it's pretty simple do solve that just adding something like onkeyup="return checkInputs(this.value) and returning false every time that the text input lenght is < 3 or > 6.

But to attend item 2) it seems that I should use Javascript setTimeout , in which this case makes use of a callback, i.e, i can not use any return to meet the event the onkeyup of my input text .

I've been struggling with this problem for a few days, and i don't have any clues how can i solve this, just because i'm limited with my actual stack (JSF 2.1 without any additional library such as PrimeFaces or OmniFaces and Jquery 1.3)

Html Code:

<h:inputText id="text" alt="text"
                maxlength="6" size="6" title="text"
                value="#{managedBean.searchText}"
                onkeyup="return checkInputs(this.value)">
                <f:ajax listener="#{managedBean.doSearch}" execute="@this"
                    event="keyup" render="form:searchResult" />
            </h:inputText>

Javascript:

function checkInputs(value) {

let cnsAnterior = sessionStorage.getItem('previousValue');
sessionStorage.setItem('previousValue', value);

if((value.length >= 3 && value.length <= 6) && (previousValue != value)){       
    return true;
}

return false;

}

Some snippet i've found at this website, for preventing search until user has stopped typing:

// Get the input box
var textInput = document.getElementById('test-input');

// Init a timeout variable to be used below
var timeout = null;

// Listen for keystroke events
textInput.onkeyup = function (e) {

    // Clear the timeout if it has already been set.
    // This will prevent the previous task from executing
    // if it has been less than <MILLISECONDS>
    clearTimeout(timeout);

    // Make a new timeout set to go off in 800ms
    timeout = setTimeout(function () {
        console.log('Input Value:', textInput.value);
    }, 500);
};

Is there any way, using JSF, that i can attend this two clauses ? Only do a server search when user typed at least 3 characters AND stopped type at few milisseconds ?

Thanks in advance.

Max
  • 478
  • 4
  • 11

2 Answers2

2

Since you are not on JSF 2.2 or up (yet) and using PrimeFaces (purely for its p:ajax) is not an option, you indeed need some 'workaround' (effectively it is not a workaround but for JSF 2.1 a good solution).

Fortunately, and many so called 'modern' javascript UI fans forget this, the JSF client side is just html, javascript and ajax. And the JSF ajax is, just like any other ajax in any other framework, handled with javascript and its basic ajax handling can be overridden in a basic javascript way as can be seen in Aborting JSF Ajax Request from jsf.ajax.addOnEvent().

I took this as a basis and combined this with some creative basic javascript timeout handling as you already suspected would be needed.

It makes use of the option to pass the original event arguments on to a not-anonymous javascript function via timeout. Like in How can I pass a parameter to a setTimeout() callback?. The hardest part was to find out how the 'arguments' would be passed. It turned out you can even pass multiple individual arguments to the setTimeout instead of one 'arguments' argument

It looks like a lot of code but a lot is comment/logging (which you can remove) and some variables can be reduced.

if (jsf) { // test if the 'jsf' javascript is available

    var originalRequest = jsf.ajax.request; // 'save' the original ajax handling
    var timeouts ={}; // keep an 'hash' of timeouts so it can be used for lots of
                      // different elements independently based on element id

    jsf.ajax.request = function(source, oevent, options) {

        // check for a 'delayedEvent' class on an input and only apply delay to those that have one
        delayed = document.getElementById(oevent.target.id).classList.contains("delayedEvent");
        if(delayed) {

            // check if an existing timer for this element is already running and clear it
            // so it will never fire but will be 'superseded' by a new one
            var timeout = timeouts[oevent.target.id];
            if (timeout != undefined) {
                console.log('Delayed event cleared: ' , arguments, timeout);
                clearTimeout(timeout);
            } 

            // create a new timeout with a 350 ms delay. Making the delay 'dynamic' could 
            // be done by creating 'composite' classnames like 'delayEvent500' and delayEvent250'. 
            // Or putting both via html 5 data attributes on a container elelement... 
            // Be creative
            timeout = setTimeout(applyOriginalEvent, 350, source, oevent, options);
            console.log("Delayed event created: ", arguments, timeout);
            timeouts[oevent.target.id]=timeout;
        } else {
            console.log('Direct event fired: ' , arguments);
            originalRequest.apply(null, arguments); // apply the request direcyly
        }
    };


    function applyOriginalEvent(source, oevent, options) {
        var timeout = timeouts[oevent.target.id];
        console.log('Delayed event fired: ' , arguments, timeout);
        // fire an ajax request with the original arguments;
        originalRequest.apply(null, arguments);
        // remove the timeout from the list
        delete timeouts[oevent.target.id];
    }

};

Just make sure this is loaded after jsf.js is loaded. I tested this locally and on the Omnifaces showcase via a browser developer console.

What I did not test it with is your existing 'length' checks but these could be left untouched.

Kukeltje
  • 12,223
  • 4
  • 24
  • 47
  • Just a small comment that oevent can be null so delayed = oevent && document.getElementById(oevent.target.id).classList.contains("delayedEvent"); would be better – Miklos Krivan Aug 23 '21 at 07:49
-1

To solve this problem IMHO you need to think outside of the JSF stack. In Angular this is a common situation solved by using the debounceTime operator from RxJS. For example:

import { AbstractControl } from '@angular/forms';
import { debounceTime, switchMap, map, first } from 'rxjs/operators';
@Injectable({ providedIn: 'root'})
export class UserNotTakenValidatorService {
    constructor(private signUpService: SignUpService) {
}
checkUserNameTaken() {
    return (control: AbstractControl) => {
        return control.valueChanges
            .pipe(debounceTime(300))
            .pipe(switchMap(userName => this.signUpService.checkUserNameTaken(userName)))
            .pipe(map(isTaken => isTaken ? {userNameTaken: true} : null ))
            .pipe(first());
        }
    }
}

In this case, the service will be called only after the user stops typing for 300 milliseconds.

Researching a little I found an integration of RxJS and Java. Check it out: https://www.baeldung.com/rx-java. If this integration for some reason doesn't help, I would try to import the RxJS library into the project.

Claudiomir
  • 109
  • 1
  • 7