34

I want to use countUp.js on my custom theme in Wordpress.

When I add the file with wp_enqueue_script(), I get an error:

Uncaught SyntaxError: Unexpected token 'export'

I've read that it can be fixed by setting type="module" on the <script> tag, but I don't know how to do that as that option doesn't exist in wp_enqueue_script()...

Can anyone help me?

8 Answers 8

60

One can add attributes to a script by applying filter 'script_loader_tag'.

Use add_filter('script_loader_tag', 'add_type_attribute' , 10, 3); to add the filter.

Define the callback function like the example given on the link above:

function add_type_attribute($tag, $handle, $src) {
    // if not your script, do nothing and return original $tag
    if ( 'your-script-handle' !== $handle ) {
        return $tag;
    }
    // change the script tag by adding type="module" and return it.
    $tag = '<script type="module" src="' . esc_url( $src ) . '"></script>';
    return $tag;
}
Sign up to request clarification or add additional context in comments.

3 Comments

do I need to register a handle first? Also, do I then also enqueue the script as normal after?
@AviG Yes, you do. You can first enqueue your script using wp_enqueue_script( 'your-script-handle', 'script-file.js' ); and then add the filter using add_filter('script_loader_tag', 'add_type_attribute' , 10, 3);
In case you need it, here is the documentation for add_filter as well: developer.wordpress.org/reference/functions/add_filter
30

As of WordPress 6.5 there is a native function for this:

wp_enqueue_script_module('my-module', get_theme_file_uri('/module.js'))

More info:

3 Comments

Finally!!! Thank you for make us aware of this.
I think this should be flagged as answer
Ayo 4 President :) - thx for the hint.
6

The wp_script_attributes filter is also handy for this and a little cleaner if all you need to do is add an attribute.

https://developer.wordpress.org/reference/hooks/wp_script_attributes/

add_filter( 'wp_script_attributes', 'add_type_attribute', 10, 1 );

function add_type_attribute( $attributes ) {
  // Only do this for a specific script.
  if ( isset( $attributes['id'] ) && $attributes['id'] === 'my-handle-js' ) {
    $attributes['type'] = 'module';
  }

  return $attributes;
}

The id comes from the handle you set when enqueuing the script. The format being <handle>-js. In this case the enqueuing could be something like:

wp_enqueue_script(
  'my-handle',
  'https://cdn.jsdelivr.net/some/url',
  array(),
  null,
  false
);

Comments

3

This is a little more complicated way to go... But I've used the following to add defer, crossorigin etc...

I don't think it's that official yet but from what I've read it's not a bad way to do it (and I've seen this approach in production for several plugins).

So, (adjust your params/variables to suit obviously) script registering (not just enqueue) is required (see https://developer.wordpress.org/reference/functions/wp_script_add_data/):

wp_register_script('countup-js', 'https://cdnjs.cloudflare.com/ajax/libs/countup.js/2.0.0/countUp.min.js', ['jquery'], $js_version, true);
wp_enqueue_script('countup-js');

wp_scripts()->add_data('countup-js', 'type', 'module');

And then your filter (like the top answer here, but I think this outlines how you can set it up to be more reusable, flexible etc)

add_filter('script_loader_tag', 'moduleTypeScripts', 10, 2);
function moduleTypeScripts($tag, $handle)
{
    $tyype = wp_scripts()->get_data($handle, 'type');

    if ($tyype) {
        $tag = str_replace('src', 'type="' . esc_attr($tyype) . '" src', $tag);
    }

    return $tag;
}

2 Comments

This adds the type='module' to the script while not removing the original type='text\javascript', I added an answer that breaks up the string to do the necessary replacements.
1

To further add to what @Zeldri said, you don't need to make use of wp_localize_script(), we can keep that function for translations as it was first intended for.

Below is the code you'd need if you don't want to lose code added using wp_add_inline_script()

function make_scripts_modules( $tag, $handle, $src ) {
    
            if ( 'your-script-handle' !== $handle ) {
                return $tag;
            }
    
            $id = $handle . '-js';
    
            $parts = explode( '</script>', $tag ); // Break up our string
    
            foreach ( $parts as $key => $part ) {
                if ( false !== strpos( $part, $src ) ) { // Make sure we're only altering the tag for our module script.
                    $parts[ $key ] = '<script type="module" src="' . esc_url( $src ) . '" id="' . esc_attr( $id ) . '">';
                }
            }
    
            $tags = implode( '</script>', $parts ); // Bring everything back together
    
            return $tags;
}
add_filter('script_loader_tag', 'make_scripts_modules' , 10, 3);

This will turn the required script into a module and leave the inline scripts alone.

Comments

1

To me Peter's answer is simpler, so this corrects the issues that answer has.

// enqueue the scripts however you like
wp_register_script('countup-js', 'https://cdnjs.cloudflare.com/ajax/libs/countup.js/2.0.0/countUp.min.js', ['jquery'], $js_version, true);
wp_enqueue_script('countup-js');

// explicitly set the type based on the id
wp_scripts()->add_data('countup-js', 'type', 'module');

// add our function to use the explicitly set type.
add_filter('script_loader_tag', 'custom_script_types', 10, 2);
function custom_script_types($tag, $handle) {
    $type = wp_scripts()->get_data($handle, 'type');
        
    if ($type) {
        // only replace the type and nothing else.
        $tag = preg_replace('/ type="(.*?)" /', 
            ' type="' . esc_attr($type) . '" ', 
            $tag);
    }
        
    return $tag;
}

This can be used for other types and only replaces the "type" and doesn't duplicate it etc.

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/countup.js/2.0.0/countUp.min.js" id="countup-js"></script>

becomes

<script type="module" src="https://cdnjs.cloudflare.com/ajax/libs/countup.js/2.0.0/countUp.min.js" id="countup-js"></script>

Comments

0

I want to address a specificity to Paul Naveda's answer. Yes it works, but with this your basically losing any additional inline scripts.

What I mean is, when using wp_add_inline_script() function, which basically takes what you are giving it, and either add it just before or after the current 's handle:

wp-includes/class.wp-scripts.php:393 (github)

$tag  = $translations . $cond_before . $before_handle;
$tag .= sprintf( "<script%s src='%s' id='%s-js'></script>\n", $this->type_attr, $src, esc_attr( $handle ) );
$tag .= $after_handle . $cond_after;
$tag = apply_filters( 'script_loader_tag', $tag, $handle, $src );

So by doing:

$tag = '<script type="module" src="' . esc_url( $src ) . '"></script>';

You loose the before and after tag. To make sure you're not losing it, either use wp_localize_script() (it's supposed to only be used for translating though) or add those lines of codes:

function add_type_attribute($tag, $handle, $src) {
    if ('your_handle_here' === $handle) {
        /** @var WP_Scripts $wp_scripts */
        global $wp_scripts;
        $before_handle = $wp_scripts->print_inline_script( $handle, 'before', false );
        $after_handle  = $wp_scripts->print_inline_script( $handle, 'after', false );
        if ( $before_handle ) {
            $before_handle = sprintf( "<script type='text/javascript' id='%s-js-before'>\n%s\n</script>\n", esc_attr( $handle ), $before_handle );
        }
        if ( $after_handle ) {
            $after_handle = sprintf( "<script type='text/javascript' id='%s-js-after'>\n%s\n</script>\n", esc_attr( $handle ), $after_handle );
        }
        $tag = $before_handle;
        $tag .= sprintf( "<script type='module' src='%s' id='%s-js'></script>\n", $src, esc_attr( $handle ));
        $tag .= $after_handle;
    }
    return $tag;
}
add_filter('script_loader_tag', 'add_type_attribute' , 10, 3);

It keeps the before and after and print it if present

Keep in mind this is still not perfect, because of $translations variable, but this is another approach if you are using wp_add_inline_script()

Comments

-1

Inspired by @PaulNaveda, @peter and @zeldri

A small plugin demontrating JS modules support, including localization and inline scripts.


Content of wp-content/plugins/js-module-support-demo/js-module-support-demo.php

<?php
/*
Plugin Name: Javascript Module Support - Demo
Plugin URI: https://froger.me/
Description: Javascript Module Support - Demo
Version: 0.1
Author: Alexandre Froger
Author URI: https://froger.me/
*/

/* ---------------------------------------------------------------
 * Below is the main logic - it can be used in plugins or a theme
 * --------------------------------------------------------------- */

add_filter( 'script_loader_tag', 'add_type_attribute', 10, 3 );
function add_type_attribute( $tag, $handle, $src ) {
    $type = wp_scripts()->get_data( $handle, 'type' );

    if ( $type && is_string( $type ) ) {
        $tag = str_replace( ' src=', 'type="' . esc_attr( $type ) . '" src=', $tag );
    }

    return $tag;
}

/* ---------------------------------------------------------------------------------------
 * Below is the demo code - it adds the demo scripts (main, module, localization, inline)
 * --------------------------------------------------------------------------------------- */

add_action( 'wp_enqueue_scripts', 'demo_scripts', 10, 1 );
function demo_scripts() {
    $inline_script = '
        console.log(\'this is an inline script added to demo.js\');
    ';

    wp_enqueue_script( 'demo', plugin_dir_url( __FILE__ ) . 'js/demo.js', array(), false, true );
    wp_scripts()->add_data( 'demo', 'type', 'module' );
    wp_add_inline_script( 'demo', $inline_script );
    wp_localize_script( 'demo', 'LocalizationVar', array( 'key' => 'value' ) );
}

Content of wp-content/plugins/js-module-support-demo/js/demo.js

import moduleVar from './module.js'

console.log(moduleVar);
console.log(window.LocalizationVar);

Content of wp-content/plugins/js-module-support-demo/js/module.js

const moduleVar = 'This is a variable from module.js';

export default moduleVar;

Upon execution of the full demo code, the following is seen in the console:

this is an inline script added to demo.js
This is a variable from module.js
{"key":"value"}

Comments

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.