Abort Primefaces autoComplete after search

Goal

To abort the request from Primefaces autoComplete component

Description

The situation: one search page with an autoComplete component to help the user while entering the search expression and a commandButton (together with a defaultCommand so that, on pressing enter, the search button is “clicked”) for the search.

The requirement: avoid displaying the suggestions if the user has already clicked the search button or pressed the ENTER/RETURN key the cursor focus is still in the autoComplete field.

How to

My first attempt to implement this feature was to group together the JSF requests in what is referred as JSF request aggregation but I thought there should be an easier way to implement this with Primefaces’ components… Next come my conclusions and the result I achieved.

I downloaded Primefaces’ source code and looked into the autocomplete.js file and I realized that there is a function there, named deleteTimeout(). Using that function could be used more or less to “abort” the triggering of the autoComplete. As a matter of fact, calling that function does not exactly cancel the request (if the query delay has timed out and the search was invoked, there is no way for us to cancel that request) but the result is appropriate most of the times (it only fails when the server takes some time to provide the results!).

After a second analysis of the code, there is a method named showSuggestions() that presents the results to the user (if the request was made, the only way to avoid showing the list of suggestions to the user is to change the behaviour of that method, which is what I did).

So, putting things in practice, all we have to do in terms of facelets is something similar to the following (notice the call to the cancelRequest() method from the widget, which is a method that will be added to that component). The call must be performed in a non ajax event such as the onClick so that it is not triggered in order with any previously pending Ajax requests:

<h:panelGroup id="search_container" layout="block">
  ...
  <p:autoComplete value="#{searchController.searchTerm}"
    placeholder="Search expression" required="true"
    completeMethod="#{searchController.complete}"
    minQueryLength="3" autoHighlight="true" cache="true"
    emptyMessage="No results for the search expression"
    widgetVar="autoCompleteWidget" />
  ...
  <p:commandButton id="searchButton"
    action="#{searchController.search()}" value="Search"
    styleClass="btn searchinput" process="search_container"
    update="search_container"
    onclick="PF('autoCompleteWidget').deleteTimeout();
             PF('autoCompleteWidget').cancelRequest();"
    onstart="PF('searchResultsWidget').getPaginator().setPage(0)" />
  <p:defaultCommand target="searchButton" />
  ...
</h:panelGroup>

And the javascript code (the application’s JS file which will define an extension to the AutoComplete module from PrimeFaces) where we defined an extension to the autoComplete component (adding a new control variable to the AutoComplete named requestCanceled and a method that makes it possible to change it to something like ‘false’ 🙂 ):

PrimeFaces.widget.AutoComplete = PrimeFaces.widget.AutoComplete
  .extend({

  init: function(cfg) {
    this._super(cfg);
    
    this.requestCanceled = false;
  },
  showSuggestions : function(query) {
    if (this.requestCanceled) {
      this.items = this.panel.find('.ui-autocomplete-item');
      this.items.attr('role', 'option');

      if (this.cfg.grouping) {
        this.groupItems();
      }

      this.bindDynamicEvents();

      var $this = this, hidden = this.panel.is(':hidden');

      if (hidden) {
        this.show();
      } else {
         this.alignPanel();
      }

      if (this.items.length > 0) {
	var firstItem = this.items.eq(0);

	// highlight first item
	if (this.cfg.autoHighlight && firstItem.length) {
	  firstItem.addClass('ui-state-highlight');
	}

	// highlight query string
	if (this.panel.children().is('ul') && query.length > 0) {
	  this.items.each(function() {
            var item = $(this), text = item.html(), re = new RegExp(
              PrimeFaces.escapeRegExp(query), 'gi'), 
              highlighedText = text.replace(re, '$&');
            item.html(highlighedText);
	  });
        }

	if (this.cfg.forceSelection) {
	  this.currentItems = [];
	  this.items.each(function(i, item) {
	  $this.currentItems.push($(item).attr('data-item-label'));
          });
	}

	// show itemtip if defined
	if (this.cfg.itemtip && firstItem.length === 1) {
	  this.showItemtip(firstItem);
	}

	this.displayAriaStatus(this.items.length + this.cfg.resultsMessage);
      } else {
        if (this.cfg.emptyMessage) {
  	  var emptyText = 
            '<div class="ui-autocomplete-emptyMessage ui-widget">'
            + this.cfg.emptyMessage + '</div>';
          this.panel.html(emptyText);
        } else {
          this.panel.hide();
        }
        this.displayAriaStatus(this.cfg.ariaEmptyMessage);
      }
    }
    this.requestCanceled = false;
  },            
  cancelRequest : function() {
    this.requestCanceled = true;
  }
})

 

Explanations

And that’s it. Calling deleteTimeout() on the autoComplete component does not abort the ajax request if it has already started but it prevents it from happening if it has not been called yet. And the new module function cancelRequest takes care of the second part of the problem (when the response has not been shown yet nor even started to be shown – ok, I won’t handle that case with this recipe, i.e., if we already started showing the response to the user, but, come on, if the solution works for 95% of the cases, it seems to be an acceptable solution, right? 😉 ).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s