Skip to content

Using Javascript in Drupal

views

Closures

Since Drupal's implementation of jQuery uses jQuery.noConflict(), it is considered good practice to wrap your custom Drupal javascript inside of a closure like this:

js
(function ($, Drupal) {
  Drupal.behaviors.myModuleBehavior = {
    ...
  };
})(jQuery, Drupal);

More info here E.g.:

js
(function ($, Drupal) {
  Drupal.behaviors.my_custom_behavior = {
    attach: function (context, settings) {
      // Code to be run on page load or refresh
      console.log('Hello, World!');
    }
  };
})(jQuery, Drupal);

Add some global JS to the theme

In booktheme.info.yml you need to specify the key to your theme library. Specifically the booktheme/global library. This refers to a key global in the booktheme.libraries.yml file. Here is the booktheme.info.yml file:

yaml
name: Book Theme
type: theme
base theme: classy
description: A flexible theme with a responsive, mobile-first layout.
package: d9book
core_version_requirement: ^10 || ^11
libraries:
  - booktheme/global
regions:
  header: 'Header'
  primary_menu: 'Primary menu'
  secondary_menu: 'Secondary menu'
  page_top: 'Page top'
  page_bottom: 'Page bottom'
  featured: 'Featured'
  breadcrumb: 'Breadcrumb'
  content: 'Content'
  sidebar_first: 'Sidebar first'
  sidebar_second: 'Sidebar second'
  footer: 'Footer'

Then in the booktheme.libraries.yml file you need a key global and under that, refer to the file: booktheme.js in the js folder of the theme. Here is the booktheme.libraries.yml file:

yaml
# Main theme library.
global:
  js:
    js/booktheme.js: {}
  css:
    base:
      css/base/elements.css: {}
    component:
      css/components/block.css: {}
      css/components/breadcrumb.css: {}
      css/components/field.css: {}
      css/components/form.css: {}
      css/components/header.css: {}
      css/components/menu.css: {}
      css/components/messages.css: {}
      css/components/node.css: {}
      css/components/sidebar.css: {}
      css/components/table.css: {}
      css/components/tabs.css: {}
      css/components/buttons.css: {}
    layout:
      css/layouts/layout.css: {}
    theme:
      css/theme/print.css: { media: print }

Then in the booktheme.js file you can add your javascript. Here is the booktheme.js file:

js
/**
 * @file
 * Book Theme behaviors.
 */

(function ($, Drupal) {

  'use strict';

  /**
   * Behavior description.
   */
  Drupal.behaviors.booktheme = {
    attach: function (context, settings) {

      console.log('It works!');

    }
  };

} (jQuery, Drupal));

You can add any key to your main libraries.yml file and then add it to the theme.info.yml file. The name is up to you. Notice here how I added a key global-stuff to the selwyn.libraries.yml file below. I also added a dependency to jQuery as jQuery is no longer loaded automatically on every page in Drupal:

yaml
global-stuff:
  js:
    js/selwyn.js: {}
  dependencies:
    - core/jquery

base:
  version: VERSION
  css:
    component:
      css/components/action-links.css:
        weight: -10
...
progress:
  version: VERSION
  css:
    component:
      css/components/progress.css:
        weight: -10
search-results:
  version: VERSION
  css:
    component:
      css/components/search-results.css: {  }
user:
  version: VERSION
  css:
    component:
      css/components/user.css:
        weight: -10

and then in my selwyn.info.yml file I added the key selwyn/global-stuff to the libraries key:

yaml
name: selwyn
type: theme
'base theme': stable9
starterkit: true
version: VERSION
libraries:
  - selwyn/base
  - selwyn/global-stuff
  - selwyn/messages
  - core/normalize
libraries-extend:
  user/drupal.user:
    - selwyn/user
  core/drupal.dropbutton:
    - selwyn/dropbutton
  core/drupal.dialog:
    - selwyn/dialog
  file/drupal.file:
    - selwyn/file
  core/drupal.progress:
    - selwyn/progress
core_version_requirement: ^10
generator: 'starterkit_theme:10.1.5'

Add JS to a module

In your module folder, add your js file. e.g. here in web/modules/general/js/jsplay.js:

js
(function ($, Drupal) {

  'use strict';

  /**
   * Behavior description.
   */
  Drupal.behaviors.logitworks = {
    attach: function (context, settings) {

      console.log('It really works!');

    }
  };

} (jQuery, Drupal));

In your general.libraries.yml file, add the key jsplay and if you need jQuery, add it as a dependency

yaml
# Custom module library for general purposes.
jsplay:
  version: 1.x
  js:
    js/jsplay.js: {}
  dependencies:
    - core/jquery

Then to get the js to load on any page content, add this to your module file:

php
/**
 * Implements hook_preprocess_HOOK().
 */
function general_preprocess_page(&$variables) {
  $variables['#attached']['library'][] =  'general/jsplay';
}

More at Adding Assets to a Drupal module via libraries

Standard JS IIFE (immediately invoked function expression)

js
(function (Drupal, $) {
  "use strict";
  // Our code here.
  console.log('Yep - more stuff working.')
}) (Drupal, jQuery);

Click to enable a dropdown menu

js
(function (Drupal, $) {

  "use strict";

  Drupal.behaviors.selwynMenuTweaker = {
    attach: function (context, settings) {
      $(context).find('#df-user-account').on('click', function() {
        flipNav();

      });
      
      function flipNav() {
        $('#df-account-dropdown').toggle(400);
        console.log('selwynclickedme');
      }

  }
};

})(Drupal, jQuery);

Cycle through some elements and add some text or css to them

js
(function (Drupal, $) {
    Drupal.behaviors.andtestthis = {
        attach: function (context, settings) {
            $('#noah').append(" and test this") ;
        }
    };
}) (Drupal, jQuery);

or using native js forEach:

js
(function (Drupal, $) {
  Drupal.behaviors.logitworks = {
    attach: function (context, settings) {

      const noah_elements = document.querySelectorAll('#noah');
      noah_elements.forEach(element => {
        // Do something with each element
        element.append('test');
      });

      const elements = document.querySelectorAll('.yomama');
      elements.forEach(element => {
        element.style.backgroundColor = 'red';
      });
    }
  };
}) (Drupal, jQuery);

Set all cards to the same height

The code in file drupal/web/sites/abc/themes/custom/uswds_base_abc/js/paragraphHeightNormalization.js will normalize the height of all paragraphs within a specific container, ensuring they all have the same height based on the tallest card. This is useful when users can put different content into each paragraph and you want them to be consistent.

The .field--name-field-clp2-requirements class targets the block field that holds the paragraphs.

The .paragraph--type--requirements-container class targets each individual paragraph within that container.

js
((Drupal) => {
  Drupal.behaviors.paragraphHeightNormalization = {
    // Exclude execution on mobile or tablet devices (width < 1024px)
    if (window.innerWidth < 1024) {
      return;
    }
    attach: (context) => {
      const paragraphContainers = context.querySelectorAll('.field--name-field-clp2-requirements');
      
      paragraphContainers.forEach(container => {
        const paragraphs = container.querySelectorAll('.paragraph--type--requirements-container');
        
        // Find maximum height
        const maxHeight = Array.from(paragraphs).reduce((max, paragraph) => {
          return Math.max(max, paragraph.offsetHeight);
        }, 0);

        // Apply max height
        paragraphs.forEach(paragraph => {
          paragraph.style.height = `${maxHeight}px`;
        });
      });
    }
  };
})(Drupal);

Don't forget to add the library to your theme's uswds_base_abc.libraries.yml file:

yaml
paragraph-height-normalization:
  version: 1.x
  js:
    js/paragraphHeightNormalization.js: {}

Add a quick function to run when the page is ready

js
(function($) {
    $(document).ready(function() {
        // Your code here.
        console.log('Yep - more stuff working.');
    });
})(jQuery);

Using Drupal.behaviors:

js
(function($) {

  // Define a namespace for your JavaScript code.
  Drupal.behaviors.myModule = {

    // This function is called when the document is ready.
    attach: function(context, settings) {

      // Add a message to the page.
      $('body').append('<p>Hello, world!</p>');
    }
  };

})(jQuery);
js
!function (document, Drupal, $) {
  'use strict';

  Drupal.behaviors.dropDownList = {

    attach: function attach(context) {
      $(document).on('click', '.js-header-dropdown__link', function () {
        // Close all popups but this.
        $('.js-header-dropdown__link')
          .not(this)
          .removeClass('is-dropdown')
          .next($('.js-header-dropdown'))
          .removeClass('is-open');

        // Enable popup.
        $(this)
          .toggleClass('is-dropdown')
          .next($('.js-header-dropdown'))
          .toggleClass('is-open');
      });

      // Close popup when clicking outside container.
      $(document).on('click', function (e) {
        if (!$(e.target).closest('.js-header-dropdown__link').length)
          $('.js-header-dropdown__link')
            .removeClass('is-dropdown')
            .next($('.js-header-dropdown'))
            .removeClass('is-open');
      });

      // Close popup when hitting `esc` key.
      $(document).keydown(function(e) {
        if (e.keyCode == 27) {
          $('.js-header-dropdown__link')
            .removeClass('is-dropdown')
            .next($('.js-header-dropdown'))
            .removeClass('is-open');
        }
      });
    }
  };
}(document, Drupal, jQuery);

Asset library overview

These are collections of css and js files

Namespaced: theme_name/library_name

There are 3 ways to use asset libraries:

  1. Info file
  2. Preprocess function
  3. {{ attach_library('classy/node') }}

Here is an example of an asset library in use:

In burger.info.yml for the theme:

yml
name: "Hamburger Theme"
description: "interesting description for the theme"
type: theme
core: 9.x
base theme: classy
libraries:
  - burger/global-styling

So to add css in that library:

yaml
global-styling:
  css:
    layout:
      css/custom.css: {}
    theme:
      css/custom.css: {}

and make a css folder in the theme folder with a file called custom.css:

css
* {
  color: red;
}

Don't forget to flush caches.

Attaching a library to specific nodes

To only load on nodes (skipping other entities), add the following function to your .theme file.

php
function mytheme_preprocess_node(&$variables) {
  $variables['#attached']['library'][] = 'mytheme/fancy-tables';
}

Add Javascript to a project using asset libraries

In this project at web/themes/custom/txglobal/txglobal.libraries.yml we are trying to load some js in the file js/globe.js only on specific pages. The asset library is defined below (see map:) and in the template file, we specify which assets to include. That causes globe.js to be loaded when that template is used.

yml
global:
  version: VERSION
  css:
    base:
      css/txglobal.css: {}
  js:
    js/txglobal.js: {}
map:
   version: 1.x,
   js:
    js/globe.js: {}

In the node--map.html.twig file at web/themes/custom/txglobal/templates/content/node--map.html.twig we attach the library txglobal.libraries.yml from above using:

twig
{{ attach_library('txglobal/map') }}

Retrieve values from select fields on a form and update other fields

js
(function ($, Drupal) {

  /**
   * Handle controlling the node add/edit for the staff profile content type.
   * 
   */
  Drupal.behaviors.areasOfExpertiseImprovements = {
    attach: function () {

      // The select field for the staff profile type.
      const typeSelect = $('.staff-profile-form #edit-field-staff-profile-type');

      // The select field for the primary title.
      const cccPrimaryTitleSelect = $('#edit-field-staff-profile-title');

...

      /**
       * Handle the areas of expertise field.
       */
      const updateAreasOfExpertise = () => {
        // Get the value of the primary title select field as an integer.
        const cccPrimaryTitle = parseInt(cccPrimaryTitleSelect.val(),10);
        // Get the value of the type select (taxonomy) field as an integer.
        const typeSelectVal = parseInt(typeSelect.val(),10);

        if (typeSelectVal === 2574) {
          handlePI(); // Type = PI.
        }
        // Type = Other Staff.
        else if (typeSelectVal === 2575) { 
          // Array of primary title tid's that are considered valid for Other Staff.
          const validCccPrimaryTitles = [2477, 2478, 2558, 2651];
          if (validCccPrimaryTitles.includes(cccPrimaryTitle)) {
            handleOtherStaffWithCccPrimaryTitle();
          } else {
            handleOtherStaff();
          }
        }
      };

...

      // Update field on the initial form load.
      updateAreasOfExpertise();

      // Update field on the Staff Profile type change.
      typeSelect.on('change', function () {
        resetAllAreasOfExpertise(true);
        updateAreasOfExpertise();
      });

      // Update fields on the Primary Title change.
      cccPrimaryTitleSelect.on('change', function () {
        updateAreasOfExpertise();
      });
    }
  };

})(jQuery, Drupal);

ESLint

ESLint can be used to make sure JavaScript code is consistent, free from syntax errors, leaking variables and that it can be properly minified.

ESLint is a Node.js module and is integrated into a number of IDEs. (See installation and usage instructions.) One of the fastest ways to install all necessary ESLint dependencies is using eslint-config-drupal.

The following configuration files ship with Drupal and can be used from there: .eslintrc.json and .eslintignore.

In a Drupal folder these configuration files will be automatically detected and used by ESLint when it is invoked from within the code base.

Installation

bash
# Before running these commands, install node.js, npm, and npx
npm install eslint --save-dev
npm i eslint-config-drupal
npx eslint modules/custom/
npx eslint themes/custom/

Resources