Drupal 8: Adding Events To Existing Behaviours

I recently needed to add functionality to the Password Policy module so I thought I would outline the steps I took in a blog post. The Password Policy module is used to enforce a stricter password policy on users on a Drupal site. This means that when a user creates or changes their password they must conform to certain rules like the password length, or if it contains upper and lower case characters. There are a set of rules to chose from and they can be fully customised by the site administrators. It's a good module, you should check it out.

I was recently working on a site that had used this module for some time. I was looking at improving the user experience across the site and one thing that stood out was that the Password Policy module only triggers once you have left the password field. I decided to build a little module that would add a real time check to the password field that would update as the user filled in their password. Due to the way in which the Password Policy module has been created I needed to override hooks and augment the existing JavaScript with some of my own.

This post was put together from my working out how to override the Password Policy module in very specific areas to add this real time checking functionality. It also shows how you can augment existing JavaScript events using a few lines of code.

The first step was to remove a hook that the Password Policy module adds to the system. The module has a hook_element_info_alter() hook that is used to add in a form process feature to the password confirmation field. As we are looking to alter this method we need to remove this hook from Drupal so that we can add out own version. It is possible to remove implemented hooks from a Drupal system using the hook_module_implements_alter() hook. We just need to find the relevant hook and ensure that it doesn't exist.

/**
 * Implements hook_module_implements_alter().
 */
function my_module_module_implements_alter(&$implementations, $hook) {
  if ($hook == 'element_info_alter' && isset($implementations['my_module'])) {
    if (isset($implementations['password_policy'])) {
      // If the password_policy module is set then remove its
      // element_info_alter hook as we will be implementing our own.
      unset($implementations['password_policy']);
    }
  }
}

With the hook_module_implements_alter() hook gone we need to create our own version. This is done by copying the Password Policy version of this hook and changing a few names. The #process element on the password confirm allows us to alter the element quite late in the processing of the element itself, which means that we can effect every password confirmation element straight away. The use of the process element here was why we needed to overwrite this method completely as there is no way to intercept this upstream.

/**
 * Implements hook_element_info_alter().
 */
function my_module_element_info_alter(array &$types) {
  if (isset($types['password_confirm'])) {
    $types['password_confirm']['#process'][] = 'my_module_password_policy_check_constraints_password_confirm_process';
  }
}

An alternative to this approach would have been to use the hook_module_implements_alter() hook to inject our version of hook_element_info_alter() to be executed after the Password Policy module version. I have written about changing hook weights in a previous post if you are interested.

With that in place we can create a our process function. This function exists in the Password Policy module and is used to inject the needed ajax options into the field. The Password Policy module adds a disable-refocus parameter to the ajax call which creates a problem for what we are trying to achieve here. This disable-refocus parameter prevents the field from being refocused after the ajax event is triggered. This is removed and a new library added to the element that we can inject JavaScript to use control the automatic update. Fundamentally, we are keeping the existing functionality and events provided by the module, but adding our own library and tweaking the existing settings.

Remember that the password confirmation element actually exists as two fields. The password field (called pass1) and the password confirmation field (called pass2) so this action is being applied to the first password field.

function my_module_password_policy_check_constraints_password_confirm_process(array $element, FormStateInterface $form_state, array $form) {
  $form_object = $form_state->getFormObject();

  if (method_exists($form_object, 'getEntity') && $form_object->getEntity() instanceof UserInterface) {
    if (_password_policy_show_policy()) {
      $element['pass1']['#ajax'] = [
        'event' => 'change',
        'callback' => '_password_policy_check_constraints',
        'method' => 'replace',
        'wrapper' => 'password-policy-status',
      ];
      $element['pass1']['#attached']['library'][] = 'my_module/password_event';
      $element['pass1']['#attributes']['class'][] = 'passwordtimer';
    }
  }

  return $element;
}

As we have added a new JavaScript library to the module we need to create a my_module.libraries.yml file for it.

password_event:
  js:
    js/password-event.js: {}
  dependencies:
    - core/jquery
    - core/drupal
    - core/drupal.ajax

With this in place we can create the js/password-event.js file. This JavaScript file is used to wrap the existing change event that the Password Policy module put in place. In reality it is Drupal that creates this JavaScript from the options in the Password Policy module, not the module itself injecting code into the site. Adding to the existing event uses some of the lessons learnt in a previous post about inspecting and reusing jQuery events. What we are doing here is pulling out the existing change event from the password field and wrapping it in a event that detects if the user has pressed a key on the same field with an offset of a second. This means that if a user stops typing then we trigger the original change event and show the user their password policy update.

(function ($, Drupal) {
  'use strict';

  Drupal.behaviors.bform = {
    attach : function(context, settings) {
      // Load the existing change event.
      var events = $._data($(".passwordtimer")[0], "events");

      // Create function to trigger the loaded event.
      function doneTyping(event) {
        $.each(events.change, function() {
          this.handler(event);
        });
      }

      var typingTimer;
      var doneTypingInterval = 1000;

      // Trigger an event when the user presses a key.
      $('.passwordtimer').keyup(function(event) {
        clearTimeout(typingTimer);
        typingTimer = setTimeout(doneTyping, doneTypingInterval, event);
        return false;
      });

    }
  };

})(jQuery, Drupal);

The reason we wait for a second before triggering this event is because otherwise the screen would be jumping around as the user types into the field. By offsetting the update by a second we can ensure that the user has broken from typing long enough to want to see the update.

With all that in place the Password Policy module now has a real time password checking feature and but still has the original check after the user has swapped fields.

Would I recommend always doing this kind of thing? Looking back on it now, a few weeks later, probably not. Although it is possible to override functionality like this in Drupal it does create some technical debt., The more you do this, the more you will open code up for errors. If the underlying module changes its structure then your code will not function and you'll be forced to revisit it. There comes a point where a complex series of hook overrides  would be better solved by a simple patch against the module, which in retrospect is probably what I should have done here.

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
4 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.