Conditionally Loading JavaScript and CSS in WordPress Plugins

Update 1/2/2013: As of WordPress 3.3, it’s now possible to call wp_enqueue_script() directly inside a shortcode callback, and the JavaScript file will be called within the document’s footer. That’s technically possible for CSS files as well, but should be considered a bad practice because outputting CSS outside the <head> tag violates W3C specs, can case FOUC, and may force the browser to re-render the page.

Also, in a related wp-hackers thread, Otto made a compelling point about balancing optimization with complexity and maintainability.

* * * *

When writing a WordPress plugin it’s considered a best practice to only include JavaScript/CSS/etc files on the specific pages that need them, rather than on every page load. This speeds up page load by reducing the number of HTTP transactions and also minimizes the risk of unnecessary conflicts between other files. There are two popular methods for achieving this for a shortcode.

The first method sets a flag inside the shortcode handler when the shortcode is called, and then checks the flag in a wp_footer() callback. If the flag is true,  wp_print_scripts() is used output the JavaScript. The problem with this approach is that it doesn’t work for CSS, because CSS should be declared inside the head tag.

The second method ‘peeks ahead’ by checking to see if the shortcode exists inside the current post’s content before wp_head() is called. This is much better because it works for CSS and allows you to use wp_enqueue_script(). The one problem with it, though, is that there’s no practical way to check if someone uses do_shortcode() in a template, rather than putting the shortcode in a post.

The workaround for that is to provide a way for a theme developer to tell the plugin which pages are calling do_shortcode(), so that the plugin will know to include the files on those pages too. You can do this by checking if the scripts are loaded in your shortcode handler, and then outputing an error if they’re not. The error can contain a link to instructions for them on how to provide the list of pages to the plugin.

I’ve implemented this in version 1.1.3 of Basic Google Maps Placemarks, so you can download it or browse the source to see it working in a live environment.

Here’s a stripped-down version of what’s happening on the plugin side. loadResources() hooks in with a priority of 11 so that it loads after the theme’s code, and it calls mapShortcodeCalled() in order to check if it should load the files on the current page.

<?php

if( !class_exists('BasicGoogleMapsPlacemarks') )
{
	/**
	* A WordPress plugin that adds a custom post type for placemarks and builds a Google Map with them
	* Requires PHP5+ because of various OOP features, json_encode(), pass by reference, etc
	* Requires WordPress 3.0 because of custom post type support
	*
	* @package BasicGoogleMapsPlacemarks
	* @author Ian Dunn <redacted@iandunn.name>
	* @link http://wordpress.org/extend/plugins/basic-google-maps-placemarks/
	*/
	class BasicGoogleMapsPlacemarks
	{
		// ...

		/**
		* Constructor
		* @author Ian Dunn <redacted@iandunn.name>
		*/
		public function __construct()
		{
			// ...

			$this->mapShortcodeCalled	= false;

			// ...

			add_action( 'wp_head',								array( $this, 'outputHead' ) );
			add_action( 'wp_footer',							array( $this, 'outputFooter' ) );
			add_filter( 'the_posts', 							array( $this, 'loadResources'), 11 );
			add_shortcode( 'bgmp-map',							array( $this, 'mapShortcode') );
		}

		/**
		* Checks the current post(s) to see if they contain the map shortcode
		* @author Ian Dunn <redacted@iandunn.name>
		* @param array $posts
		* @return bool
		*/
		function mapShortcodeCalled($posts)
		{
			$this->mapShortcodeCalled = apply_filters( self::PREFIX .'mapShortcodeCalled', $this->mapShortcodeCalled );
			if( $this->mapShortcodeCalled )
				return true;

			foreach( $posts as $p )
			{
				preg_match( '/'. get_shortcode_regex() .'/s', $p->post_content, $matches );
				if( is_array($matches) &amp;amp;&amp;amp; array_key_exists(2, $matches) &amp;amp;&amp;amp; $matches[2] == 'bgmp-map' )
					return true;
			}

			return false;
		}

		/**
		* Load CSS and JavaScript files
		* @author Ian Dunn <redacted@iandunn.name>
		*/
		public function loadResources($posts)
		{
			// @todo - maybe find an action that gets run at the same time. would be better to hook there than to a filter. update faq for do_shortcode if do

			wp_register_script(
				'googleMapsAPI',
				'http'. ( is_ssl() ? 's' : '' ) .'://maps.google.com/maps/api/js?sensor=false',
				false,
				false,
				true
			);

			wp_register_script(
				'bgmp',
				plugins_url( 'functions.js', __FILE__ ),
				array( 'googleMapsAPI', 'jquery' ),
				self::BGMP_VERSION,
				true
			);

			wp_register_style(
				self::PREFIX .'style',
				plugins_url( 'style.css', __FILE__ ),
				false,
				self::BGMP_VERSION,
				false
			);

			if( $posts )
			{
				$this->mapShortcodeCalled = $this->mapShortcodeCalled( $posts );

				if( !is_admin() &amp;amp;&amp;amp; $this->mapShortcodeCalled )
				{
					wp_enqueue_script('googleMapsAPI');
					wp_enqueue_script('bgmp');
				}

				if( is_admin() || $this->mapShortcodeCalled )
					wp_enqueue_style( self::PREFIX . 'style' );
			}

			return $posts;
		}

		/**
		* Outputs elements in the <head> section of the front-end
		* @author Ian Dunn <redacted@iandunn.name>
		*/
		public function outputHead()
		{
			if( $this->mapShortcodeCalled )
			require_once( dirname(__FILE__) . '/views/front-end-head.php' );
		}

		/**
		* Outputs some initial values for the JavaScript file to use
		* @author Ian Dunn <redacted@iandunn.name>
		*/
		public function outputFooter()
		{
			if( $this->mapShortcodeCalled )
			require_once( dirname(__FILE__) . '/views/front-end-footer.php' );
		}

		/**
		* Defines the [bgmp-map] shortcode
		* @author Ian Dunn <redacted@iandunn.name>
		* @param array $attributes Array of parameters automatically passed in by WordPress
		* return string The output of the shortcode
		*/
		public function mapShortcode($attributes)
		{
			if( !wp_script_is( 'googleMapsAPI', 'queue' ) || !wp_script_is( 'bgmp', 'queue' ) || !wp_style_is( self::PREFIX .'style', 'queue' ) )
				return '<p class="error">'. BGMP_NAME .' error: JavaScript and/or CSS files aren\'t loaded. If you\'re using do_shortcode() you need to add a filter to your theme first. See <a href="http://wordpress.org/extend/plugins/basic-google-maps-placemarks/faq/">the FAQ</a> for details.</p>';

			$output = sprintf('
				<div id="%smap-canvas">
				<p>Loading map...</p>
				<p><img src="%s" alt="Loading" /></p>
				</div>',
				self::PREFIX,
				plugins_url( 'images/loading.gif', __FILE__ )
			);

			return $output;
		}

		// ...

	} // end BasicGoogleMapsPlacemarks
}

?>

The theme developer can put this code in their functions.php file to tell the plugin which pages to load the files on:

add_filter( 'the_posts', 'my_theme_name_bgmp_shortcode_check' );
function my_theme_name_bgmp_shortcode_check( $posts )
{
	$shortcodePageSlugs = array(
		'first-page-slug',
		'second-page-slug',
		'hello-world'
	);

	if( $posts )
		foreach( $posts as $p )
			if( in_array( $p->post_name, $shortcodePageSlugs ) )
				add_filter( 'bgmp_mapShortcodeCalled', 'your_theme_name_bgmp_shortcode_called' );

	return $posts;
}

function your_theme_name_bgmp_shortcode_called( $mapShortcodeCalled )
{
	return true;
}

19 thoughts on “Conditionally Loading JavaScript and CSS in WordPress Plugins

  1. Hello,

    Regarding the second “issue” you mentioned:

    No, it doesn’t use wp_enqueue_script(), but it does use wp_register_script(), which would still prevent the same script for loading twice.

    It’s never a good idea to follow best practices blindly.

  2. Hey scribu, I don’t think I’m just blindly following the best practice. You’re right that your method would avoid enqueuing files multiple times because it registers the script, but that’s not the only reason wp_enqueue_script() is the best practice.

    It’s also important to let other plugins/themes dequeue your script if they want to replace it with their own. I haven’t run tests to confirm it, but it doesn’t look like that’d be possible using wp_print_scripts(). Let me know if I’m wrong and I’ll remove that point from the post.

  3. Ok, I guess my beef was that you didn’t say why it was an issue from the beginning.

    Anyway, you do have a point. I’ve updated my code to call wp_register_script() on ‘init’.

    That way, other plugins have the opportunity to replace or remove the script.

  4. Hey Ian,

    I read your post with big excitement and also the post of scribu about ‘optimal script loading’. With my limited understanding of php programming and how all the hooks in WordPress are actually operating, I tried to come up with a solution for conditional script and style loading from the functions.php.
    In some cases it works fine with just conditions that are related to a certain page of which I know the $post->ID.
    For example I am checking if there is a custom field with a certain value on the homepage and I am loading javascripts in dependence of that.

    The function to check for a custom field on the homepage looks like this:
    function hp_option($szKey) {
    $homepage = get_option(‘page_on_front’); // id of homepage
    $cfv_homepage = get_post_meta( $homepage, $szKey, true );
    return $cfv_homepage;
    }

    Than I can easly use this here later in the fucntions.php:
    $cufon = hp_option(‘use_cufon’);
    and use the variable/value to enqueue scripts conditionally.

    I have tried for a very long time to achive this flexibility on a post to post basis with getting custom field values and loading styles and scripts conditionally. It doesn’t work if I don’t know the $post->ID.
    I tried many ways to get this to work within the functions.php:
    $js_tabs = get_post_meta($postID, ‘jQuery Tabs -Extra’, true); // load JS for jQuery Tabs
    It so far never worked out.
    Also a function did not work out like it works out for the homepage.
    function guest_author_name( $value = ” ) {
    global $wp_query;
    $postID = $wp_query->post->ID;
    $key = get_post_meta($postID, $value );
    return $key;
    }

    Do you see any possibility to get custom field values from posts and pages on a post to post basis within the functions.php?
    I want to check for custom field values on each page and if they are there do something regarding the inclusion of styles and scripts.
    For example the inclusion of a javascript only when a checkbox ‘Use jQuery Tabs’ is checked in an opion metabox based on Advanced Custom Fields.

    It would be great if you can help me out with some information.
    I searched a lot for information to solve this ‘problem’ and this website comes closest to what I am looking for, so maybe you know how to do it because I still don’t have a clue. :)

  5. Sorry for the many comments but I just found this, which seems to do the job perfectly:
    add_action(“wp_head”,”add_conditional_scripts”, 20);
    function add_conditional_scripts() {
    global $posts, $wp_scripts;
    $event_page = get_field(‘events_page’); // Page with a list of events

    foreach ($posts as $post) {
    if ($event_page == true) :
    wp_enqueue_script( ‘scripthandle’ , get_home_url() .’/js/scriptfilename.js’, false, ‘1.0’, true); // This will add to the $wp_scripts variable
    endif;
    }

    wp_print_scripts(); // This uses the $wp_script object, will print, out the newly enqueue script
    }

    I also tried scribu’s code for simple inclusion based on shortcodes and it works like a charm as well:

    add_shortcode(‘myshortcode’, ‘my_shortcode_handler’);

    function my_shortcode_handler($atts) {
    wp_enqueue_script(‘my-script’, plugins_url(‘my-script.js’, __FILE__), array(‘jquery’), ‘1.0’, true);

    // actual shortcode handling here
    }

    Wow, I was really looking up for these things again and again for weeks.

  6. My ‘solution’ was unfortunatly only half a solution. I was so enthusiastic about my alleged success that I oversaw a crucial detail.
    My scripts are now all in the header of the website!
    That’s actually not what I want.

    This little line causes the confusion:
    add_action(“wp_head”,”add_conditional_scripts”, 20);

    The whole function – posted above – forces even all other scripts (which I enqued through another function) into the header, even when that function is actually applied with:
    add_action(‘wp_footer’, ‘my_script_management’, 100);

    Do you know if there is any way to make it work with including the scripts through the wp_footer hook?
    I want all my scripts to be in the footer.

  7. Wow, that was a trip to get to the answer. And it is so f*** simple.
    wp_reset_query();
    And my orginal function works. So what I use is basically a function which is than called like this.
    // load/unload scripts for the theme
    add_action(‘wp_print_scripts’, ‘my_script_management’);
    add_action(‘wp_footer’, ‘my_script_management’, 100);

    And I am happy!

    • I found you posting your question in a couple of different places Sebastian and was so excited to see your solution here!
      However, I was hoping that you could post the solution with a little more detail as to where you’re placing the code. For example, where are you placing the wp_reset_query(); that you discovered was necessary for your previous code to work?
      Are you placing this all in functions.php?

      • Hi Mike,

        with the help of a programmer friend I found a clean solution that is completely based on code in the functions.php.

        // get custom field values in the functions.php – in a reliable way

        function get_cfield_vars($cached = true) {

        global $wp_query;
        static $vars = null; // set static variable, our “cache”, is persistant between function calls

        // $cached is by default TRUE
        // if get_cfield_vars(false) is called, cache will be ignored
        if (!is_null($vars) && $cached === true) {
        return $vars;
        }

        $vars = new stdClass();

        // page to page conditions
        $vars->form = get_field(‘form_page’, $wp_query->queried_object_id); // checks if there is a cft (form) in the page

        return $vars;

        }

        // and than I can use code like this

        add_action(‘wp’, ‘register_my_script’);

        function register_my_script() {

        // related to /extra/functions_inc_customfields.php
        $cf_vars = get_cfield_vars();

        if($cf_vars->form) wp_enqueue_script(‘cForms’, $script_path . ‘cforms.js’, false, ‘1.0’, false);

        }

        Let me know if you have more questions.

          • For some reason I had to take the caching out to make it work with the newest WordPress version. The rest still works just fine for me.

            I am wondering of there is a smarter solution out there.

  8. Finally I also found a solution for conditional css loading which works for my plugin http://www.mapsmarker.com and I´d like to share with you. It checks if my shortcode is used within the current template file and header/footer.php and if yes, enqueues the needed stylesheet in the header:

    function prefix_template_check_shortcode( $template ) {
    $searchterm = ‘[mapsmarker’;
    $files = array( $template, get_stylesheet_directory() . DIRECTORY_SEPARATOR . ‘header.php’, get_stylesheet_directory() . DIRECTORY_SEPARATOR . ‘footer.php’ );
    foreach( $files as $file ) {
    if( file_exists($file) ) {
    $contents = file_get_contents($file);
    if( strpos( $contents, $searchterm ) ) {
    wp_enqueue_style(‘
    leafletmapsmarker’, LEAFLET_PLUGIN_URL . ‘leaflet-dist/leaflet.css’);
    break;
    }
    }
    }
    return $template;
    }
    add_action(‘template_include’,’prefix_template_check_shortcode’ );

Leave a Reply

Your email address will not be published. Required fields are marked *