Site icon WP Smith

WordPress JavaScript Manager Native Functions

WordPress has native PHP JavaScript management functions. Using these functions are better than manually adding <script> tags in the <head> or footer of the page. In this article, we will discuss:

  1. PHP Constants for Debugging JavaScript
  2. Registering and Deregistering Scripts
  3. Enqueueing and Dequeueing Scripts
  4. Localizing Scripts
  5. Initializing Scripts
  6. Modifying Script Tags on Print

PHP Constants for Debugging JavaScript

Most WordPress developers are familiar with WP_DEBUG and know how to use that to debug WordPress. WP_DEBUG is designed as a global for turning debugging on and off, but WP_DEBUG has no affect on JavaScript files. Fewer developers know about SCRIPT_DEBUG. While WP_DEBUG helps debug the PHP code, SCRIPT_DEBUG helps with JavaScript. SCRIPT_DEBUG will output the normal, non-minified version of the JavaScript files. For example, with SCRIPT_DEBUG will output wp-embed.js while normally WordPress will output wp-embed.min.js. You can use this constant to output minified and non-minified your plugin/theme JavaScript files dynamically. For example:

<?php
namespace MyPrefix\MyPluginOrTheme;
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// JavaScript File
$filename = "myscript{$suffix}.js";
// CSS File
$filename = "myscript{$suffix}.css";

Alternatively, you could use a function that will do this for you:

<?php
namespace MyPrefix\MyPluginOrTheme;
/**
* Gets the proper JavaScript filename based on SCRIPT_DEBUG.
*
* @param string $filename Filename without the extension.
*
* @return string JavaScript filename.
*/
function get_js_filename( $filename ) {
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
return "{$filename}{$suffix}.js";
}
$filename = get_js_filename( 'myscript' );

Or, you can use a constant or multiple constants:

<?php
namespace MyPrefix\MyPluginOrTheme;
// Define MULTIPLE Suffixes
define( 'JS_SUFFIX', ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.js' : '.min.js' ) );
define( 'CSS_SUFFIX', ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '.css' : '.min.css' ) );
// JavaScript file
$filename = 'myscript' . JS_SUFFIX;
// CSS file
$filename = 'mystyle' . CSS_SUFFIX;
// Define a SINGLE Suffix
define( 'SCRIPT_SUFFIX', ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min' ) );
// JavaScript file
$filename = 'myscript' . SCRIPT_SUFFIX . '.js';
// CSS file
$filename = 'mystyle' . SCRIPT_SUFFIX . '.css';
]

Or (just for fun) create a function for the suffix only:

<?php
namespace MyPrefix\MyPluginOrTheme;
/**
* Gets the proper JavaScript filename based on SCRIPT_DEBUG.
*
* @param string $filename Filename without the extension.
*
* @return string JavaScript filename.
*/
function get_suffix( $filename ) {
static $suffix;
if ( null !== $suffix ) {
return $suffix;
}
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
return $suffix;
}
$filename = 'myscript' . get_suffix() . 'js';

If SCRIPT_DEBUG is on, the filename will be my-js-file.js while if SCRIPT_DEBUG is false, the filename will be my-js-file.min.js. So if you have a Gulp/Grunt script or a PHPStorm File Watcher auto-minifying your JavaScript, WordPress will properly output the correct JavaScript file.

Registering and Deregistering Scripts

Why register your scripts?

  1. DRY: The script manager allows you to re-use and output the script in various places of your code.
  2. Integrations: The script manager allows for anyone to integrate, optimize, and build upon your script.
  3. Prevents Duplication: A script using the script manager will only be printed on the page once.
  4. Client Focused & Client Friendly: It allows your customers/clients to remove the scripts easily.

DRY

The first reason (and one which I agree) is perfect for coders who subscribe to DRY (Don't Repeat Yourself) coding, which most of us prefer. It optimizes your code and makes your code clean and readable. WordPress Core registers a ton of JavaScript files. Most notable of these are:

If you are using any of these, please use the WordPress core version. It ensures the best experience.

Client Focused & Client Friendly

Many developers do not like the second reason. They want to control every aspect of their code because they are afraid that their client/customer or another developer will break their plugin. However, to me, this is not a good reason. If a customer/client breaks the plugin/site because they chose to copy/paste code and place it in their functions.php site, it is their fault, and they can pay you to fix it. But by providing the client/customer code that is changeable, it makes you look like a better developer, and you will have return customers/clients because they can trust that you have their best interests in mind.

Integrations & Prevents Duplication

Plugins like WP Rocket and other optimization plugins can detect and handled registered scripts much easier than functions that echo script elements at random places throughout the document. WordPress filters, like script_loader_src and script_loader_tag, can be used to amend how one or more registered scripts are loaded in the DOM by amending the tag attributes (see below for the advanced examples), and finally other scripts can use those scripts as dependencies themselves.

By using a script manager, WordPress tries to prevent duplication of scripts. For example, if one plugin outputs underscore via a script tag and the other developer uses WordPress's version, then multiple underscore scripts will be printed on the page, which obviously will lead to problems.

The Code: Registering/Deregistering Scripts

There is a function for registering a JavaScript file (wp_register_script) and one for deregistering a JavaScript file (wp_deregister_script). Now, many developers, and even those on developers.wordpress.org, recommend that you register scripts within the wp_enqueue_scripts hook. However, it is my opinion that you should always register scripts on init (for frontend scripts wrapped with ! is_admin() conditional) and admin_init (for admin scripts) hooks. For the purposes of this article, we will be focusing all of the examples to frontend JavaScript, so we won't be using the admin_init hook. This allows your customers/clients to manipulate the scripts however they need, if they need.

Registering JavaScript Files

wp_register_script($handle, $src, $deps, $ver, $in_footer) takes a couple parameters which are obvious: $handle (slug) $src (the script URL), $in_footer (whether to output in the head or footer). It is always recommended that $in_footer be true unless (1) there is a very specific reason for the script to be in the head (other than "my code doesn't work in the footer") or (2) you use defer. Two really good examples are Google Analytics and New Relic—both require to be run in the head at the top. IMHO Google Analytics should have the async attribute added to it. The two remaining parameters—dependencies ($deps) and version ($ver) are more fascinating.

Script Version ($ver)

Most people use a simple version number to identify the version (e.g., 0.0.1 or 2.1). In fact, most developers are either ignorant or lazy and only use the version of the theme or plugin via a constant or something hardcoded that never seems to change. So if you are doing development with an aggressively caching browser it becomes problematic and frustrating to ensure that your changes are being reflected in the DOM, even with browsersync or gulp-livereload. However, the best option is to do both the version of the plugin and the file version. If you are like me, changing the version of the file every time in the JavaScript file and the correlating PHP file is such a hassle. Being a stickler on this yet also lazy about versioning myself, I need something simple that would be cacheable and yet also cache bust when needed (on update/change). My goal is to load the JavaScript file via WordPress and forget about that part and work on the JavaScript file only. So, I use filemtime for file versioning within my PHP and maintain semantic versioning within the JavaScript file properly. filemtime helps me accomplish this. It returns the epoch time (e.g., 1529330834) of when the file was written/changed. So as long as the file does not change or update, the file will be cached.

So here is how I do versioning for my scripts:

<?php
namespace MyPrefix\MyPluginOrTheme;
define( 'PLUGIN_VERSION', '1.0.0' );
// Parent Theme Version
$ver = PLUGIN_VERSION . '-' . filemtime( get_template_directory() . "/myscript$suffix.js" );
// Child Theme Version
$ver = PLUGIN_VERSION . '-' . filemtime( get_stylesheet_directory() . "/myscript$suffix.js" );
// Plugin Version, assumes this PHP file is in the root of the plugin folder (e.g., ./wp-content/plugins/myplugin/script-dependencies.php)
$ver = PLUGIN_VERSION . '-' . filemtime( get_plugin_dir( __FILE__ ) . "/myscript$suffix.js" );

The resulting filename will be something like myscript.js?ver=1.0.0-1528229721. This will make it easy to identify the version of the plugin while also ensuring that the correct version of the script is on the frontend. Even WordPress.com does something similar outputting files with the version of the file and a date (e.g., https://s1.wp.com/wp-content/js/mustache.js?ver=6.1.1-201824).

Script Dependencies ($deps)

Finally, $deps are really cool because when I output the file, WordPress will also output all the dependencies without me having to do it...but more on that in the next section.

Registering Scripts in Themes

For themes, you register files like this (using the previous best practice with the dynamic suffix):

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the theme folder.
wp_register_script(
'myscript', // file slug
get_stylesheet_directory_uri() . "/js/myscript$suffix.js", // file URL
array(), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/myscript$suffix.js" ), // version
true // in footer or not
);
wp_register_script(
'jquery-myscript', // file slug
get_stylesheet_directory_uri() . "/js/jquery.myscript$suffix.js", // file URL
array( 'jquery' ), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/jquery.myscript$suffix.js" ), // version
true // in footer or not
);
}

Alternatively, you could use a theme script register helper:

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
/**
* Register my scripts.
*
* @param string $filename Filename.
* @param string $theme_relative_path Relative path to file within theme folder.
*/
function register_theme_script( $filename, $theme_relative_path = '/', $dependencies = array(), $footer = true ) {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
wp_register_script(
str_replace( '.', '-', $filename ), // file slug without js extension
get_stylesheet_directory_uri() . "{$theme_relative_path}{$filename}{$suffix}.js", // file URL
$dependencies, // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "{$theme_relative_path}{$filename}{$suffix}.js" ), // version
$footer // in footer or not
);
}
Registering Scripts in Plugins

For plugins, you register files like this (using the previous best practice with the dynamic suffix):

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'myscript',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/myscript{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/myscript{$suffix}.js" ), // version
true
);
wp_register_script(
'jquery-myscript',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/jquery.myscript{$suffix}.js", // file URL
array( 'jquery' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/jquery.myscript{$suffix}.js" ), // version
true
);
}

Alternatively, you could use a plugin script register helper:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
// Should be placed at the root of the plugin folder
define( 'MY_PLUGIN_FILE', __FILE__ );
/**
* Register my scripts.
*
* @param string $filename Filename.
* @param string $theme_relative_path Relative path to file within theme folder.
*/
function register_plugin_script( $filename, $theme_relative_path = '/', $dependencies = array(), $footer = true ) {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
wp_register_script(
str_replace( '.', '-', $filename ), // file slug without js extension
get_plugin_dir( MY_PLUGIN_FILE ) . "{$theme_relative_path}{$filename}{$suffix}.js", // file URL
$dependencies, // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "{$theme_relative_path}{$filename}{$suffix}.js" ), // version
$footer // in footer or not
);
}
Deregistering JavaScript Files

Deregistering is extremely straight forward as you can see here:

<?php
namespace MyPrefix\MyPluginOrTheme;
// Deregister script by handle
wp_deregister_script( 'myscript' );

However, actually deregistering a script can be quite tricky. The main problem with deregistering scripts is deregistering AFTER it has been registered. For plugins, this is why registering a script and enqueueing (or just enqueueing) a script in the same function (without a do_action between them) is an unfriendly and bad idea.

For example, here are three bad ways to do it:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_enqueue_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
}
<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
wp_enqueue_script( 'plugin-script' );
}
https://gist.github.com/6e676ff6c5516427bb04aac361cacf00

This last one looks that most right but the problem is that they both are hooked at the same priority and there is no way to hook in between the functions to do anything.

Here is a better way:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts', 99 );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script' );
}

This way, I can hook in between the functions within wp_enqueue_scripts hook to make any changes necessary.

Here is the best way (IMHO):

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script' );
}

So when do you deregister a script? You deregister a script for two primary reasons:

  1. If you want to re-register the script with different arguments (primarily $src URL).
  2. If you want to ensure the script never gets placed on the frontend period.

Warning: Whenever you deregister a script (e.g., myscript) without re-registering a new version of that script, any script dependent upon that script (e.g., myscript) will not output either.

So, consider this example. In wp-config.php, we have define( 'SCRIPT_DEBUG', true );. Now, let's say we have this in an active plugin:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script-base',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js", // file URL
array( 'underscore' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array( 'plugin-script-base' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script-ext',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js", // file URL
array( 'plugin-script' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts', 999 );
/**
* Outputs our plugin scripts
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script-ext' );
}

With a single statement, our use of the active plugin outputs three files on the frontend in this order:

Now in our theme, say we have this in the functions.php file:

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\deregister_scripts', 999 );
/**
* Register my scripts.
*/
function deregister_scripts() {
// Assumes the JS file is located in the js folder within the plugin folder.
wp_deregister_script( 'plugin-script-base' );
}

This function within functions.php will cause plugin-script and plugin-script-ext NOT to be placed on the frontend either because the WordPress script manager assumes that since plugin-script and plugin-script-ext are dependent upon plugin-script-base, they also won't work if plugin-script-base is missing. So WordPress will not output those files.

A Note

Many seasoned non-WordPress PHP developers will want to use their own version of jQuery. So they will deregister WordPress's jQuery and register their own. This may be a great idea for your own project ONLY that you will always control, but it is a terrible idea for any free/premium plugins and/or themes. Here's why:

This is not client/customer friendly at all. As a developer, we need to get over our own opinions that using a CDN version or a specific version of a script is better and accept this part of WordPress and the WordPress ecosystem. Simply, play nice!

Enqueueing and Dequeueing Scripts

So far, we have only properly registered (or deregistered) the scripts within WordPress but that does not output the script tags on the frontend. To output the scripts on the frontend, you need to queue the script to be outputted by the WordPress script manager. To do this, you will need to use wp_enqueue_script.

wp_enqueue_script does two things:

  1. Registers the script if it has not been registered already.
  2. Outputs any files on the frontend during the wp_head hook via print_head_scripts (which calls wp_print_scripts hook) and wp_footer hook via _wp_footer_scripts which calls print_late_styles() and print_footer_scripts().

Sometimes, however, plugins and themes will just enqueue all their scripts even if the page doesn't use that particular script. They do this mostly because they provide their functionality through a shortcode and don't know how to properly test the page to determine whether that shortcode exists or not (blog post coming soon). In these cases, to increase the performance of your site, you may want to remove them. To do this, you will need to use wp_dequeue_script. Let's say that our script was "properly" registered on init and enqueued on wp_enqueue_scripts hooks. To dequeue the script, you just need to do this:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\remove_script' );
/**
* Register my scripts.
*/
function remove_script() {
// Always dequeue the script
wp_dequeue_script( 'plugin-script' );
// Conditionally dequeue the script on archive pages
if ( is_archive() ) {
wp_dequeue_script( 'plugin-script' );
}
}

Localizing Scripts

Sometimes, our scripts need some additional information or configuration for whatever reason (localizing strings, configuring a JavaScript object, etc). This is done via wp_localize_script. wp_localize_script takes three parameters: $handle (string), $object_name (string), and $l10n (array).

When you localize your script, the object name (the second parameter) is the name of your JavaScript object, so best practice is that this parameter string would be camelCase. There are no checks within WordPress, so if you pass something like 'my-object', your JavaScript will fail.

Whatever PHP array is passed to wp_localize_script will be placed ahead of your external script tag. You can organize the $l10n array however you'd like, but over time, I have learned and now prefer to do something like this:

$plugin_l10n = array(
'strings' => array(
'firstLabel' => __( 'Some string to translate', 'textdomain' ),
'secondLabel' => __( 'Some string to translate', 'textdomain' ),
),
'config' => array(
'something' => 'some-value',
'somethingElse' => 3,
),
'debug' => ( ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ( ! defined( 'SCRIPT_DEBUG' ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) ),
);

Again, please note that the string properties are camelCased.

Your JavaScript object will be placed in the Global Namespace. In our example, the JavaScript object is named pluginScriptExt. Therefore, our object is available globally via window.pluginScriptExt, so you must always prefix your objects unless you overwrite another object (if you need more information, see Mozilla's Global Object definition).

For example, if we take our script plugin-script-base from above, it is dependent on underscore. Underscore has a global JavaScript object named _. If we were to do this, we would render Underscore useless and _.each would not be available; instead _.strings would be.

// Overwrites underscore
wp_localize_script( 'plugin-script-ext', '_', $plugin_script_base_l10n );

Alternatively, calling wp_localize_script twice on the same script with the same object name will overwrite the object, not extend the object. For example,

// Create empty pluginScriptExt object -- won't work
wp_localize_script( 'plugin-script-ext', 'pluginScriptExt', array() );
// Add .config to pluginScriptExt object -- won't work
wp_localize_script( 'plugin-script-ext', 'pluginScriptExt', array(
'strings' => array(
'firstLabel' => __( 'Some string to translate', 'textdomain' ),
'secondLabel' => __( 'Some string to translate', 'textdomain' ),
),
) );
// Add .strings to pluginScriptExt object -- won't work
wp_localize_script( 'plugin-script-ext', 'pluginScriptExt', array(
'config' => array(
'something' => 'some-value',
'somethingElse' => 3,
),
) );

With this example, you will have:

// Overwritten
var pluginScriptExt = {
config: {
something: 'some-value',
somethingElse: 3
}
};

So, if you want to "expand" an object, it is better to create two objects (until WP_Scripts expands its capabilities).

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script-base',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js", // file URL
array( 'underscore' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-base{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array( 'plugin-script-base' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
wp_register_script(
'plugin-script-ext',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js", // file URL
array( 'plugin-script' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-ext{$suffix}.js" ), // version
true
);
// Create base object for pluginScriptExt object.
wp_localize_script( 'plugin-script-ext', 'pluginScriptExtStrings', array(
'firstLabel' => __( 'Some string to translate', 'textdomain' ),
'secondLabel' => __( 'Some string to translate', 'textdomain' ),
) );
// Create base object for pluginScriptExt object.
wp_localize_script( 'plugin-script-ext', 'pluginScriptExtConfig', array(
'something' => 'some-value',
'somethingElse' => 3,
) );
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts', 999 );
/**
* Outputs our plugin scripts
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script-ext' );
}

With this example, you will have:

// As 2 objects
var pluginScriptExtStrings = {
firstLabel: 'Some string to translate', // localized
secondLabel: 'Some string to translate', // localized
};
var pluginScriptExtConfig = {
something: 'some-value',
somethingElse: 3
};

It is important to note that the script should either be registered (via wp_register_script) or enqueued (via wp_enqueue_script) before it can be localized.

Initializing Scripts

There are several scripts, like sliders, that require some sort of initialization. Take waypoints, for example. It requires initialization before it can be used on the page. For example:

var waypoint = new Waypoint({
element: document.getElementById('waypoint'),
handler: function(direction) {
console.log('Scrolled to waypoint!')
}
});

To do this properly within WordPress, you need to use wp_add_inline_script.

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my frontend scripts.
*/
function register_scripts() {
if ( is_admin() ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js" ), // version
true
);
wp_register_script(
'my-waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js", // file URL
array( 'waypoints' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'my-waypoints' );
wp_add_inline_script( 'my-waypoints', "var waypoint = new Waypoint({
element: document.getElementById('waypoint'),
handler: function(direction) {
console.log('Scrolled to waypoint!')
}
})" );
}
// Result
//<script type="text/javascript" src="https://domain.com/wp-content/plugins/myplugin/js/my-waypoints.js"></script>
//<script type="text/javascript">
// var waypoint = new Waypoint({element: document.getElementById('waypoint'),
// handler: function(direction) {
// console.log('Scrolled to waypoint!')
// }
// })
//</script>

wp_add_inline_script can also output JavaScript before a specific script tag. To do this:

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my frontend scripts.
*/
function register_scripts() {
if ( is_admin() ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js" ), // version
true
);
wp_register_script(
'my-waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js", // file URL
array( 'waypoints' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/my-waypoints{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'my-waypoints' );
wp_add_inline_script( 'my-waypoints', 'console.log("before my-waypoints file")', 'before' );
}
// Result
//<script type="text/javascript">
// console.log("before my-waypoints file")
//</script>
//<script type="text/javascript" src="https://domain.com/wp-content/plugins/myplugin/js/my-waypoints.js"></script>

If you use wp_localize_script and wp_add_inline_script, wp_localize_script will always output before wp_add_inline_script.

Using wp_add_inline_script is a great way to keep your code clean using native WordPress functions and the script manager avoiding yet another hook and manually outputting this yourself.

It is important to note that the script should either be registered (via wp_add_inline_script) or enqueued (via wp_add_inline_script) before it can be initialized.

Extending the previous Waypoints example, let's only do something if waypoints is not already registered. You do this by using wp_script_is.

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Do nothing if the script is registered
if ( wp_script_is( $handle, 'registered' ) ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'waypoints',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/waypoints{$suffix}.js" ), // version
true
);
}

With wp_script_is, you can check whether a script has been:

wp_script_is is quite powerful in debugging scripts and in trying to find when a script was registered, enqueued, and printed on the page.

For example, with wp_script_is( $handle, 'enqueued' ), you can know that you can either deregister (if false) or dequeue the script (if true) in order to prevent that script from being printed on the page.

Finally (though this is becoming much less important), you can make your scripts conditional by adding conditional data to the script. You can do this by using

<?php
namespace MyPrefix\MyPlugin;
define( 'MY_PLUGIN_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my frontend scripts.
*/
function register_scripts() {
if ( is_admin() ) {
return;
}
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the plugin folder.
wp_register_script(
'plugin-script',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js", // file URL
array(), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script{$suffix}.js" ), // version
true
);
// IE only Script
wp_register_script(
'plugin-script-ie',
get_plugin_dir( MY_PLUGIN_FILE ) . "js/plugin-script-ie{$suffix}.js", // file URL
array( 'plugin-script' ), // dependencies
MY_PLUGIN_VERSION . '-' . filemtime( plugin_dir_url( MY_PLUGIN_FILE ) . "js/plugin-script-ie{$suffix}.js" ), // version
true
);
}
add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\wp_enqueue_scripts' );
/**
* Register my scripts.
*/
function wp_enqueue_scripts() {
wp_enqueue_script( 'plugin-script' );
wp_script_add_data( 'plugin-script', 'conditional', 'lt IE 9' );
}

Modifying Script Tags on Print

Adding Additional Attributes to script tag—async and defer

WordPress does not automatically add async or defer to your script tags, nor is there an "easy" way to add it to the script manager (which is still being actively discussed in core (tickets 22249 and 12009).

First, what is the difference between defer and async? bitsofcode has two great diagrams that explains the difference between the two:

Simply stated both defer and async download/fetch the script immediately and do not pause/prevent further HTML parsing. The difference comes at JavaScript execution. defer executes after the HTML parsing is completed and DOM is ready. Because of this adding defer to scripts in the footer makes little sense. async executes when the script comes ready.

To improve the performance of the page, you need to add these yourself. You can do this by using the script_loader_tag filter, which gives you the HTML of the script being printed on the page, including the conditional HTML and any added inline script.

To add async or defer to all, you can do this:

<?php
namespace MyPrefix\MyTheme;
add_filter( 'script_loader_tag', __NAMESPACE__ . '\script_loader_tag_add_async', 10 );
/**
* Filters the HTML script tag of an enqueued script.
*
* @param string $tag The `<script>` tag for the enqueued script.
* @param string $handle The script's registered handle.
* @param string $src The script's source URL.
*
* @return string Modified script tags including `async="async"`.
*/
function script_loader_tag_add_async( $tag ) {
if ( false === strpos( $tag, 'async' ) ) {
return str_replace( ' src', ' async="async" src', $tag );
}
}
add_filter( 'script_loader_tag', __NAMESPACE__ . '\script_loader_tag_add_defer', 10 );
/**
* Filters the HTML script tag of an enqueued script.
*
* @param string $tag The `<script>` tag for the enqueued script.
* @param string $handle The script's registered handle.
* @param string $src The script's source URL.
*
* @return string Modified script tags including `async="async"`.
*/
function script_loader_tag_add_defer( $tag ) {
if ( false === strpos( $tag, 'defer' ) ) {
return str_replace( ' src', ' defer="defer" src', $tag );
}
}

Alternatively, you can be more specific and do something like this:

<?php
namespace MyPrefix\MyTheme;
add_filter( 'script_loader_tag', __NAMESPACE__ . '\script_loader_tag', 10 );
/**
* Filters the HTML script tag of an enqueued script.
*
* @param string $tag The `<script>` tag for the enqueued script.
* @param string $handle The script's registered handle.
* @param string $src The script's source URL.
*
* @return string Modified script tags including `async="async"`.
*/
function script_loader_tag( $tag ) {
// Load AFTER a page has finished loading completely
$deferred = array( 'comment-reply', 'wp-emoji' );
foreach ( $deferred as $script ) {
if ( false !== strpos( $tag, $script ) && false === strpos( $tag, 'defer' ) ) {
return str_replace( ' src', ' defer="defer" src', $tag );
}
}
// Load asynchronously while page is loading
$async = array( 'wp-embed', 'myscript' );
foreach ( $async as $script ) {
if ( false !== strpos( $tag, $script ) && false === strpos( $tag, 'async' ) ) {
return str_replace( ' src', ' async="async" src', $tag );
}
}
return $tag;
}

If you use the Genesis Framework, this is already built-in for you. All you need to do is register your script with either ?async=true or ?defer=true and Genesis will automagically add async or defer to the script tag for you!!

<?php
namespace MyPrefix\MyTheme;
define( 'MY_THEME_VERSION', '1.0.0' );
add_action( 'init', __NAMESPACE__ . '\register_scripts' );
/**
* Register my scripts.
*/
function register_scripts() {
// Suffix
$suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
// Assumes the JS file is located in the js folder within the theme folder.
wp_register_script(
'myscript', // file slug
get_stylesheet_directory_uri() . "/js/myscript$suffix.js?defer=true", // file URL with ?async=true added
array(), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/myscript$suffix.js" ), // version
true // in footer or not
);
wp_register_script(
'jquery-myscript', // file slug
get_stylesheet_directory_uri() . "/js/jquery.myscript$suffix.js?async=true", // file URL with ?defer=true added
array( 'jquery' ), // dependencies
MY_THEME_VERSION . '-' . filemtime( get_stylesheet_directory() . "/js/jquery.myscript$suffix.js" ), // version
true // in footer or not
);
}

Changing the Script Source—Cache Busting

You can use the script_loader_src filter to cache bust everything.
DO NOT USE THIS IN PRODUCTION.

<?php
namespace MyPrefix\MyTheme;
add_filter( 'script_loader_src', __NAMESPACE__ . '\script_loader_src', 10, 2 );
/**
* Cache bust all the scripts.
*
* @param string $src Script loader source path.
* @param string $handle Script handle.
*
* @return string
*/
function script_loader_src( $src, $handle ) {
if ( 'plugin-script' !== $handle ) {
return $src;
}
return add_query_arg( 'wps', time(), $src );
}

Summary

With this overview of the native WordPress Script functions for script management, you should be able to use any type of JavaScript file in any plugin or theme extending parent theme or plugin JavaScript files.

  1. PHP Constants for Debugging JavaScript: SCRIPT_DEBUG allows you to manage whether your script outputs minified or not. You can also pass the value of SCRIPT_DEBUG to wp_localize_script to help you add debug messages within the console.
  2. Registering and Deregistering Scripts: Use wp_register_script to register a script on init hook and wp_deregister_script only if you plan to register an alternative to avoid any unintended effects.
  3. Enqueueing and Dequeueing Scripts: Use wp_enqueue_script to output the script on the page and wp_dequeue_script to prevent the script's output.
  4. Localizing Scripts: Use wp_localize_script to output any configuration and translations of any strings used within your script.
  5. Initializing Scripts: Use wp_add_inline_script to add additional inline scripts to initialize external scripts or any thing else than adding a configuration object.
  6. Modifying Script Tags on Print: Use script_loader_src and script_loader_tag to make more advanced changes to the script tag including adding attributes like async or defer, which Genesis makes easy for you.

Thank you to both Gary Jones and Mike Hemberger for pre-reviewing and providing some great insights!