📜 ⬆️ ⬇️

Drupal: ajax_facets and history API

Probably every web developer faced with the need to implement a search on the site. Quite a common solution - Apache Solr. In the world of Drupal development, this is no exception. For the integration of Solr with Drupal and the implementation of faceted search, there are modules search_api , search_api_solr and facetapi . But in most cases, we would like the search results and the facet filters to be updated without reloading the page, that is, ajax. And, as usual in the Drupal world, on d.org there is some time and user-tested module (or maybe not proven, as lucky) that does what we need. In this case, ajax_facets .

Ajax facets is a module that provides several types of widgets that can be used in faceted search filters. These are “range slider”, “multiple checkboxes”, “selectbox” and “links”. When values ​​in these “widgets” change, the filters and the search result are updated with ajax. Great. But it would be even better if the module were friends with the history API. That is, it would save every filter state in the history, which would allow users to go through the search history with the back and forth buttons in the browser, again, without reloading the page.

Task


Of course, the need for this feature and the interest in implementation did not arise by itself. One of the projects was tasked with making ajax_facets friends with the history API. What I want to tell you.
')

Decision


As is usually the case, solving a problem begins with finding a complete solution or at least a patch. There was no ready solution, but a patch was found . Judging by the description on the issue tracker of the project, he did just what he needed. But, unfortunately, the patch was old and valid only for the old branch of the module (7.x-2.x). The idea is very simple: save the current state of the filters to the browser history at the moment when ajax_facets receives a successful response from the server to update the search result and the filters themselves. And by clicking on the “back” and “forward” buttons, retrieve the saved state of filters from the history and send a request to update the filters and search results with parameters from the saved state.

To test the idea as such, I ported the found patch to the current module branch (7.x-3.x). Everything worked. However, it required improvements. Namely, I would like this feature to work in old browsers that do not support the history API. The task is simple. There is a history.js that emulates the history API. On the other hand, I didn’t want to add a hard dependency on this library, since this would mean adding a libraries module to the dependencies. Such a patch just no one would have accepted. Imagine, you update the ajax_facets module, and in dependencies it has got libraries, which you don’t need. You don’t need the support of old browsers in the form of history.js either (you simply don’t support old browsers, for example). To avoid such situations, I decided to make everything a bit more flexible:

  1. On the server side, we check the availability of the libraries module and the history.js library. If dependencies are found, then we pass the flag “history.js is available to the front-end side; you can use the history API”.
  2. On the client side, we check if the browser supports the history API (natively or through history.js). If yes, then we do everything as expected. Otherwise, we get the standard behavior of ajax_facets (as it was before the patch).


Implementation


The first item is achieved as follows:
We give out hints on the “Status report” page if the dependencies are not found.
/** * Implements hook_requirements(). */ function ajax_facets_requirements($phase) { $requirements = array(); $t = get_t(); switch ($phase) { case 'runtime': $description = $t('For now browser ajax history feature works only in HTML5 browsers. If you want to get this feature on HTML4 browsers you need to install libraries module and download history.js library.'); $value = $t('Libraries module not installed.'); if (module_exists('libraries')) { if (!libraries_get_path('history.js')) { $description = $t('For now browser ajax history feature works only in HTML5 browsers. If you want to get this feature on HTML4 browsers you need to download history.js library.'); $value = $t('Library history.js not found.'); } else { $description = $t('For now browser ajax history feature works both in HTML4 and HTML5 browsers.'); $value = $t('Works with history.js library'); } } $requirements['ajax_facets_message'] = array( 'title' => $t('Ajax Facets'), 'description' => $description, 'value' => $value, 'severity' => REQUIREMENT_INFO, ); break; } return $requirements; } 


And we forward the flag to the front-end side in case history.js library is found.
 /** * Add required JS and handle single inclusion. */ function ajax_facets_add_ajax_js($facet) { static $included = FALSE; if (!$included) { ... // Add history.js file if exists. if (module_exists('libraries')) { $history_js_path = libraries_get_path('history.js'); if ($history_js_path) { $history_js_exists = TRUE; drupal_add_js($history_js_path . '/scripts/bundled/html4+html5/jquery.history.js', array('group' => JS_LIBRARY)); } } ... $facet = $facet->getFacet(); $setting['facetapi'] = array( .... 'isHistoryJsExists' => $history_js_exists, ); drupal_add_js($setting, 'setting'); drupal_add_library('system', 'drupal.ajax'); } } 


The implementation of the second item is shown on the example of the pushState wrapper function:
 /** * Pushes new state to browser history. * * History.js library fires "statechange" event even on API push/replace calls. * So before pushing new state to history we should unbind from this event and after bind again. */ Drupal.ajax_facets.pushState = function (state, title, stateUrl) { // If history.js available - use it. if (Drupal.settings.facetapi.isHistoryJsExists) { var $window = $(window); $window.unbind('statechange', Drupal.ajax_facets.reactOnStateChange); History.pushState(state, title, stateUrl); $window.bind('statechange', Drupal.ajax_facets.reactOnStateChange); } else { // Fallback to HTML5 history object. if (typeof history.pushState != 'undefined') { history.pushState(state, title, stateUrl); } } }; 


By the way, there is one interesting feature in history.js that needs to be taken into account: the statechange event is triggered when the browser history buttons are pressed, as well as when the history is programmatically updated, for example by calling the History.pushState () method. In the native implementation of the history API by browsers, there is an onpopstate event that is triggered only when clicking on the browser history buttons. To avoid unnecessary triggering of the statechange, you need to unsubscribe from this event before updating the browser history, and then subscribe to it again.

Conclusion


It is not always possible to find and apply a ready-made solution. But this is very cool. This makes it possible to understand, to see how the module that was used before, works inside. And, in the end, it's just nice when the solution you proposed is commited to a popular project. This means the next time someone else will not have such a problem.

Full diff can be viewed here .

Source: https://habr.com/ru/post/283282/


All Articles