Interactive web experiences provide a more engaging and enjoyable experience for users. It leads to increased user satisfaction and a positive perception of a website. For example, a form that provides instant feedback and validation, rather than making the user wait for a page refresh, can significantly improve the user experience.

JavaScript plays an important role in Drupal by providing the means to create dynamic and interactive experiences for users on the frontend of a Drupal website. It enables developers to modify the behavior of certain elements on a page, such as forms, links, or any other DOM elements, without having to refresh the entire page. Drupal Behaviors are JavaScript functions that get executed when specific events occur on a page. Behaviors make it easy for developers to maintain and upgrade a site as they don’t need to change any underlying HTML code. Find out all you wanted to know about Drupal Behaviors in the article.

Taming JavaScript in Drupal

What are Drupal Behaviors?

Drupal.behaviors is an object inside the Javascript structure in Drupal, which allows us to attach functions to be executed at certain times during the execution of the application. It is called when the DOM is fully loaded, but these behaviors can be called again. Drupal’s official JavaScript documentation suggests that modules should implement JavaScript by attaching logic to Drupal.behaviors.

Why do we need Drupal behaviors?

The advantage of Behaviors is that they are automatically re-applied to any content that is loaded through AJAX. They can be called at any time with a context that represents new additions or changes to the DOM. This is better than $(document).ready() or document DOMContentLoaded where the code is just run once.

When are Drupal behaviors unwanted?

Drupal behaviors are not always the perfect solution for writing Javascript in Drupal. In some cases, as stated below Drupal behaviors are not needed at all!

  • When we need to execute some code that does not affect the DOM. Eg. Initializing an external script like Google Analytics
  • When some JS operation needs to be performed on the DOM just once knowing that the element will be available when the page loads (This scenario is different from using Once).

When are Drupal Behaviors called?

  • After an administration overlay has been loaded into the page.
  • After the AJAX Form API has submitted a form.
  • When an AJAX request returns a command that modifies the HTML, such as ajax_command_replace().

Other times when Drupal Behaviors are invoked

  • CTools calls it after a modal has been loaded.
  • Media calls it after the media browser has been loaded.
  • Panels calls it after in-place editing has been completed.
  • Views calls it after loading a new page that uses AJAX.
  • Views Load More calls it after loading the next chunk of items.
  • JavaScript from custom modules may call Drupal.attachBehaviors() when they add or change parts of the page.

Writing code without Drupal behaviors

In this code, we are adding a click event listener to the .views-row class which calculates the number of times we are clicking on this row. But it is added only once to the elements which come in the DOM during the initial page load. After clicking on Load More and loading more items, the click listener does not work on the newly loaded items.

// No Drupal Behaviors

(function () {


 let header = document.querySelector(".food-list-header");


 if (header) {


   let greatFoodSpan = document.createElement("span");


   greatFoodSpan.textContent = "Get ready for great food!!!!!!";


   header.append(greatFoodSpan);


 }


 // Add the event listener for each click on the food


 let foods = document.querySelectorAll(".views-row");


 foods.forEach((food) => {


   food.addEventListener("click", () => {


     let foodCounter = food.querySelector(".food-click-counter");


     let timesClicked = parseInt(foodCounter.textContent.trim());


     foodCounter.textContent = ++timesClicked;


   });


 });


})();

event listener

How do we use Drupal Behaviors?

Answer: Using the attach method

Things to remember:

  • The new object needs to have at least an attach method. 
  • Anytime Drupal.attachBehaviors is called, it will iterate through all behavior objects and call their respective attach methods.

Adding Drupal behavior to our code

After adding Drupal Behaviors, the code looks something like this.

(function (Drupal) {


 Drupal.behaviors.exampleBehaviour1 = {


   attach: (context, settings) => {


     // Add a delicious text to the top of the document


     let header = document.querySelector(".food-list-header");



     // jQuery Equivalent


     // $(".food-list-header");


     if (header) {


       let greatFoodSpan = document.createElement("span");


       greatFoodSpan.textContent = "Get ready for great food!!!!!!";


       header.append(greatFoodSpan);


     }



     // Add the event listener for each click on the food


     let foods = document.querySelectorAll(".views-row");


     foods.forEach((food) => {


       food.addEventListener("click", () => {


         let foodCounter = food.querySelector(".food-click-counter");


         let timesClicked = parseInt(foodCounter.textContent.trim());


         foodCounter.textContent = ++timesClicked;


       });


     });


   },


 };


})(Drupal);

 But something odd appears in the top when we click on Load More:

behavior

 

This is because Drupal behavior is called a lot of times and subsequently we get some unintended behavior.

What is Context in “Drupal context”?

  • When calling the attach method for all behaviors, Drupal passes along a context parameter. 
  • The context parameter that is passed can often give a better idea of what DOM element is being processed.
  • During the initial page load this will be the complete HTML Document; during subsequent calls, this will be just the elements that are being added to the page or get modified.

How to add Context?

The previous problem can be solved by using the context parameter that is provided by Drupal Behaviors. In this case, the first time the page loads, we get the whole HTML Document as context and that’s when we attach the header. For further operations, it will be the part of the code which is affected by Drupal Behaviors and hence that part of the code is safely controlled.

(function (Drupal) {

 Drupal.behaviors.exampleBehaviour2 = {


   attach: (context, settings) => {


     // Add a delicious text to the top of the document.


     // The context parameter now can be used for adding


     // certain functionality which removes unwanted repeatability


     let header = context.querySelector(".food-list-header");



     // jQuery Equivalent


     // $(".food-list-header", context);


     if (header) {


       let greatFoodSpan = document.createElement("span");


       greatFoodSpan.textContent = "Get ready for great food!!!!!!";


       header.append(greatFoodSpan);


     }


     // Add the event listener for each click on the food


     let foods = context.querySelectorAll(".views-row");


     foods.forEach((food) => {


       food.addEventListener("click", () => {


         let foodCounter = food.querySelector(".food-click-counter");


         let timesClicked = parseInt(foodCounter.textContent.trim());


         foodCounter.textContent = ++timesClicked;


       });


     });


   },


 };


})(Drupal);

Again there is some odd behavior when we click on Load More. The food items which were initially loaded work fine. But After clicking on Load More, the new items get the click listener and work normally. But the initially loaded items get the listener attached again and clicking on them calls the click event more than once!

Load more

 

When do Drupal Behaviors start misbehaving?

  • Writing all the event listeners inside Drupal behaviors without using Once and Context.
  • Declaring unwanted functions inside Drupal behaviors which leads to the redeclaration of functions every time the attach method is called.

“Once” to the rescue

  • Once ensures that something is processed only once by adding a data-once attribute in a DOM element after the code has been executed.
  • If the behavior is called again, the element with the data-once attribute is skipped for further execution.
  • Once is a modern implementation of jQuery.once (which is an endeavor to move away from jQuery)
  • Once, in combination with context, controls the entire functionality perfectly as we need it.

Adding Once to fix the event listeners in our code

(function (Drupal, once) {


 Drupal.behaviors.exampleBehaviour3 = {


   attach: (context, settings) => {


     once("food-header-initialized", ".food-list-header", context).forEach(


       (header) => {


         let greatFoodSpan = document.createElement("span");


         greatFoodSpan.textContent = "Get ready for great food!!!!!!";


         header.append(greatFoodSpan);


       }


     );



     // jQuery Equivalent


     // $(".food-list-header", context).once("food-header-initialized", function (header) {


     //


     // });


     // Add the event listener for each click on the food


     once("food-initialized", ".views-row", context).forEach((food) => {


       food.addEventListener("click", () => {


         let foodCounter = food.querySelector(".food-click-counter");


         let timesClicked = parseInt(foodCounter.textContent.trim());


         foodCounter.textContent = ++timesClicked;


       });


     });


   },


 };


})(Drupal, once);

data once

 

Now everything works as intended. We get a data-once attribute to the elements where the event listeners are attached and newly loaded elements and previously loaded elements function properly.

The Need for Detach method

The Detach method acts like an anti-hero (not evil), removing whatever we did in the attach method. Any code in the detach method will be called whenever content is removed from the DOM. This helps us to clean up our application. For example, Detach method enables us to remove unwanted event listeners which consume resources like a continuous polling situation.

Examples of Detach

Assume that we have an ajax form to fill and we are using a timer to show the time elapsed. We use setTimeOut to manage the timer. We log this timer in the console for monitoring.

(function (Drupal, once) {


 let counter = 0;


 Drupal.behaviors.exampleBehaviour4 = {


   attach: (context, settings) => {


     once("timer-initalized", ".contact-timer", context).forEach((ele) => {


       const timer = context.querySelector(".contact-timer-sec");


       timer.textContent = counter;



       // Set the timer for user to see the time elapsed


       setInterval(() => {


         console.log("This is logging");


         const timer = document.querySelector(".contact-timer-sec");


         timer.textContent = ++counter;


       }, 1000);


     });


   },


 };


})(Drupal, once);

set timeout

 

On form submission, the timer on DOM gets removed but the console starts throwing an error. This is because the element on which the setTimeOut is acting has been removed from DOM:

detach

 

To avoid this we can use the detach method like this:

(function (Drupal, once) {

 let counter = 0;

 let intervalStopper;


 Drupal.behaviors.exampleBehaviour4 = {


   attach: (context, settings) => {


     // Set the timer for user to see the time elapsed


     once("timer-initialized", ".contact-timer", context).forEach((ele) => {


       const timer = context.querySelector(".contact-timer-sec");


       timer.textContent = counter;


       intervalStopper = setInterval(() => {


         const timer = document.querySelector(".contact-timer-sec");


         timer.textContent = ++counter;


         console.log("This is logging");


       }, 1000);


     });


   },


   // Clear the timer on confirmation


   detach: (context, settings, trigger) => {


     const timer = context.querySelector(".contact-timer-sec");


     if (trigger == "unload" && timer) {


       clearInterval(intervalStopper);


     }


   },


 };


})(Drupal, once);

This removes the timer on unload and as seen from the logger, the error does not occur.

JS Demo

Immediately Invoked Function Expressions (IIFE) - The wrapper for JS

We have been using IIFE to write our Drupal code. The initial opening parentheses define an anonymous function which helps prevent the function's scope from polluting the global scope of the entire application. You can pass arguments to your anonymous function by including them as arguments at the end of the function definition. 

This also helps us to namespace the parameters however we want them to be used.

Example:

// Function name crisis!!!!
// The function is vulnearble to
// be replaced by some other function
function someFunction() {
 // Some code for this function
}


(function (Drupal) {
 // Function name crisis averted!
 function someFunction() {
   // Some code for this other function
 }


 Drupal.behaviors.exampleBehaviour6 = {
   attach: (context, settings) => {
     someFunction();
   },
 };
})(Drupal);

Final Thoughts

Implementing Drupal behaviors allows for dynamic interactivity, streamlined user interaction, improved user feedback, efficient development and overall enhanced user experience of your website. Drupal.behaviors are flexible and modular, in that they can be executed multiple times on a page, can override and extend existing behavior, and can be automatically re-applied to any content loaded through Ajax. 

Looking for a Drupal development agency to help you build interactive web experiences, making the best out of Drupal? We’d love to talk!

Contact us

LET'S DISCUSS YOUR IDEAS. 
WE'D LOVE TO HEAR FROM YOU.

CONTACT US