3

Continuing from this question:

I have scripts enqueued in functions.php file so scripts load only on certain pages (I cut most of them in code below):

add_action( 'template_redirect', 'plugin_is_page' );
function plugin_is_page() {
    if ( is_search() ) {
        wp_enqueue_script('ads-search', get_template_directory_uri() . '/js/ads-search.js', array( 'ads' ), $timestamp = filemtime( plugin_dir_path( __FILE__ ) . '/js/ads-search.js' ), true);
    }
}

Now, each of them uses dependency ads.js file witch contains array of objects (ads) and functions to display them. In files like ads-search.js, I only call said functions on search results page.

I need to load latest version of each file every time (every time it changes). For now it is enqueued like this, with banners.js file it loads on every single page.

function my_custom_theme_scripts() {
    $ads_version = filemtime( get_stylesheet_directory() . '/js/ads.js' );
    $banners_version = filemtime( get_stylesheet_directory() . '/js/ads-banners.js' );

    wp_enqueue_script(
        'ads',
        get_stylesheet_directory_uri() . '/js/ads.js',
        array(),
        $ads_version,
        true
    );
    wp_enqueue_script(
        'ads-banners',
        get_stylesheet_directory_uri() . '/js/ads-banners.js',
        array( 'ads' ),
        $banners_version,
        true
    );
}
add_action( 'wp_enqueue_scripts', 'my_custom_theme_scripts' );

So how do I use functions coded in dependency ads.js file across all my scripts like ads-banners.js or ads-search.js?

Importing them wont work even if I load scripts as modules, because then I would need to include version in import line and doing just import { selectADS, sidebarADS, mobileADS } from "./ads.js"; won't work.

All functions are coded in the same way, example one:

export async function selectADS(adNumber, element, adsArray) {
  let usedNum = [];
  let i = 0;
  while (1) {
    var randomNum = Math.floor(Math.random() * adsArray.length);

    if (!usedNum.includes(randomNum)) {
      let link = document.createElement("a");
      link.setAttribute("href", adsArray[randomNum].link);
      link.setAttribute("target", "_blank");

      let ad = document.createElement("img");
      ad.setAttribute("alt", adsArray[randomNum].alt);
      ad.setAttribute("src", adsArray[randomNum].src);

      element.appendChild(link);
      link.appendChild(ad);

      usedNum.push(randomNum);
      i++;
      if (i == adNumber) break;
    }
  }
}

When I try to call selectADS in another file it throws function is not defined error.

So, how can I enqueue modules in WordPress, so that live changes to ads.js file in loaded properly without error?

3
  • Attach the functions to the window object and call them the same way eg. window.selectADS = () => {} and window.selectADS() Commented Feb 14, 2023 at 14:33
  • "I would need to include version in import line" - see dynamic module loading, which allows something like import( './ads.js?ver=' + ADS_SCRIPT_VERSION ).then( module => module.selectADS( ... ) ). Or just turn your ads module into a "normal script", i.e. without the export statement... But what you're asking is a JS question, so I voted to close it.
    – Sally CJ
    Commented Feb 14, 2023 at 16:09
  • @SallyCJ part of this question is WP related, since WP doesn't directly support type="module" yet. So IMHO, this question shouldn't be closed.
    – Fayaz
    Commented Feb 14, 2023 at 17:16

1 Answer 1

4

The problem

Import statement like:

import { export1 } from "./module-name.js";

is called static import declaration.

Dynamic query string (to force load the latest file) cannot be used in static import declarations. So, to solve this problem, you'll have to choose one of the following four options:

  1. Remove export and import statements from all the JavaScript files and enqueue them as non-module JavaScript.

  2. Don't enqueue ads.js in WordPress, only pass version number of ads.js file as a JavaScript global variable with wp_add_inline_script() function to the page, and dynamically import ads.js as a module from banners.js and ads-search.js.

  3. Enqueue ads.js as a module, pass version number of ads.js file as a JavaScript global variable with wp_add_inline_script() function to the page, enqueue banners.js and ads-search.js as non-module JavaScript (with ads.js in the dependency array), and dynamically import ads.js as a module from banners.js and ads-search.js. This is slightly different from option-2 and may have some benefits depending on implementation.

  4. Use a bundler like Rollup or Webpack and build a single minified script using a file watcher and enqueue the build minified JavaScript file in WordPress as a non-module JavaScript file. I'd recommend this method, but would not explain it here as it out of the scope of this question.

Option-1: Make all JS files non-module

Just remove export and import statements from all the JavaScript files, like below:

// in ads.js
// removed the export declaration
async function selectADS(adNumber, element, adsArray) {
  let usedNum = [];
  let i = 0;
  while (1) {
    var randomNum = Math.floor(Math.random() * adsArray.length);

    if (!usedNum.includes(randomNum)) {
      let link = document.createElement("a");
      link.setAttribute("href", adsArray[randomNum].link);
      link.setAttribute("target", "_blank");

      let ad = document.createElement("img");
      ad.setAttribute("alt", adsArray[randomNum].alt);
      ad.setAttribute("src", adsArray[randomNum].src);

      element.appendChild(link);
      link.appendChild(ad);

      usedNum.push(randomNum);
      i++;
      if (i == adNumber) break;
    }
  }
}

// in banners.js
// call selectADS function normally, no import declaration
selectADS();

Then in the WordPress end, enqueue them in my_custom_theme_scripts() as you've written in the question.

Option-2: pass ads.js version to JS & dynamically import ads.js

If for some reason you have to keep the export declaration in ads.js, and still want to import the latest file in banners.js and ads-search.js, you'll need to make some changes in WordPress enqueue code and in the JavaScript code of banners.js and ads-search.js.

For example, your my_custom_theme_scripts() function can be like this:

function my_custom_theme_scripts() {
    $ads_version = filemtime( get_stylesheet_directory() . '/js/ads.js' );
    $banners_version = filemtime( get_stylesheet_directory() . '/js/banners.js' );

    wp_enqueue_script(
        'banners',
        get_stylesheet_directory_uri() . '/js/banners.js',
        array(),
        $banners_version,
        true
    );
    wp_add_inline_script(
        'banners',
        'const My_Custom_Ads_Version = "' . $ads_version . '";',
        'before'
    );
}

In this case, ads.js file can be like before with export declaration, and banners.js file can dynamically import it like below:

(async () => {  
    if( typeof My_Custom_Ads_Version !== 'undefined' ) {
        const adsModule = await import( `./ads.js?ver=${My_Custom_Ads_Version}` );
        const selectADS = adsModule.selectADS;
        selectADS();
    }
})();

You'll have to make similar changes to ads-search.js. After that your code will work.

Remember, in this case export declarations can be like before (in ads.js), but the import statements in banners.js and ads-search.js should be changed from static import declaration to dynamic import statements as shown in the code above.

Option-3: Enqueue ads.js as a module, pass ads.js version to JS & dynamically import ads.js

This option is very similar to option-2 above. However, option-2 imports ads.js in runtime. So if an implementation wants to delay the execution of ads.js module objects, but still wants to load the ads.js module with page load, then this implementation will be preferable.

To do this, you'll have to first hook into script_loader_tag filter, so that WordPress adds type="module" in <script> tag.

Following is an example WordPress code that implements it:

add_filter( 'script_loader_tag', 'my_custom_module_support', PHP_INT_MAX, 2 );
function my_custom_module_support( $tag, $handle ) {
    if( strpos( $handle, 'my_custom_module_' ) === 0 ) {
        if( current_theme_supports( 'html5', 'script' ) ) { 
            return substr_replace( $tag, '<script type="module"', strpos( $tag, '<script' ), 7 );
        }
        else {
            return substr_replace( $tag, 'module', strpos( $tag, 'text/javascript' ), 15 );
        }
    }

    return $tag;
}
function my_custom_theme_scripts() {
    $ads_version = filemtime( get_stylesheet_directory() . '/js/ads.js' );
    $banners_version = filemtime( get_stylesheet_directory() . '/js/banners.js' );

    // we are loading ads.js as a module.
    // my_custom_module_support() function checks any handle
    // that begins with "my_custom_module_" and adds type="modle"
    // to print it as a ES6 module script 
    wp_enqueue_script(
        'my_custom_module_ads',
        get_stylesheet_directory_uri() . '/js/ads.js',
        array(),
        $ads_version,
        true
    );
    wp_add_inline_script(
        'my_custom_module_ads',
        'const My_Custom_Ads_Version = "' . $ads_version . '";'
    );
    wp_enqueue_script(
        'banners',
        get_stylesheet_directory_uri() . '/js/banners.js',
        array( 'my_custom_module_ads' ),
        $banners_version,
        true
    );
}
add_action( 'wp_enqueue_scripts', 'my_custom_theme_scripts' );

With this option as well, ads.js file can be like before with export declaration, and banners.js and ads-search.js can dynamically import from ads.js as shown in option-2 above.

However, with this option, the browser will load ads.js module file before it is imported dynamically in banners.js. So when the dynamic import is performed, the browser will just use the already loaded file.

In most cases this is not significant. However, it can be significant when you perform the dynamic import on an event like below code (especially if ads.js file takes significant time to load):

// sample ads.js code
console.log( 'ads.js loaded:START' );
export async function selectADS( adNumber, element, adsArray ) {
    console.log( 'ads.js as a module loaded' );
};
console.log( 'ads.js loaded:END' );

// banners.js code
console.log( 'banners.js load:START' );

let clicked = false;
document.addEventListener( 'click', async (event) => {   
    if( ! clicked && typeof My_Custom_Ads_Version !== 'undefined' ) {
        const adsModule = await import( `./ads.js?ver=${My_Custom_Ads_Version}` );
        const selectADS = adsModule.selectADS;
        selectADS();
    }
    clicked = true;
});

console.log( 'banners.js load:END' );

With this implementation, if the ads.js file is significantly large and it's only dynamically imported on an event, code becomes simpler without having to wait for the imported file.

However, this is not the most significant advantage, because you can rewrite that JS code to still import early. More significant advantage of this implementation is if you use a caching system or a bundler that watches changes in HTML code. With this option it's easier to handle such scenario as opposed to option-2 (because with option-2 adj.js is not visible from HTML).

6
  • "3. Enqueue ads.js as a module ... and dynamically import ads.js as a module from banners.js and ads-search.js" - why would it be enqueued, if it's going to be dynamically imported?
    – Sally CJ
    Commented Feb 15, 2023 at 2:15
  • @SallyCJ I've updated the answer, please take a look.
    – Fayaz
    Commented Feb 15, 2023 at 3:21
  • Ok, but the author said, "I need to load latest version of each file everytime".
    – Sally CJ
    Commented Feb 15, 2023 at 3:33
  • No, not every time, every time it's changed. There's a significant difference.
    – Fayaz
    Commented Feb 15, 2023 at 3:35
  • 1
    Yes, "latest version", and I did notice the use of filemtime(). It's just I did not have a chance to edit that comment before you replied. Anyway, +1.
    – Sally CJ
    Commented Feb 15, 2023 at 4:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.