Automatic Javascript Includer Helper

This article is also available in the following languages:
By predominant
A quick and easy auto-magic JavaScript includer.

Often times you will have Javascript code that is specific to a particular controller, or to a specific action of a specific controller. In order to minimise the amount of data that is sent to a client, it would be really handy to only have code that is required sent across as each request is mode.


The following helper checks for the existence of files named the same as your CakePHP controllers and actions. If these files exist, then they are automatically included as part of the pages HEAD, and sent to the client. If the controller / action javascript file doesn't exist, then nothing is added to the page scripts.


Alright, so how does it work? Its configurable, and those that want to change the default location can do so through the options. However the default is as follows:


Consider we have a UsersController and a PostsController, each with actions: index, add, edit. On accessing the UsersController index action, the helper will check for the existence of WWW_ROOT/js/autoload/users/index.js and if found, include that file. It will also check for the existence of WWW_ROOT/js/autoload/users.js for javascript that is to be included for all actions on the UsersController.


An example layout of directory structure and files:


  • webroot
    • js
      • autoload
        • users
          • index.js
          • add.js
          • edit.js
        • posts
          • index.js
          • add.js
          • edit.js
        • users.js
        • posts.js

Usage couldnt be easier. In your AppController, include the helper. Yes. Thats all you need to do.


Controller Class:

<?php 
class AppController extends Controller {
    public 
$helpers = array('AutoJavascript');
}
?>

Here is the helper code:


Helper Class:

<?php 
/** File: auto_javascript.php **/
/**
 * CakeTime JavaScript Helper
 *
 * Facilitates JavaScript Automatic loading and inclusion for page specific JS
 *
 * @copyright   Copyright 2009, Graham Weldon (http://grahamweldon.com)
 * @link        http://grahamweldon.com/projects/caketime CakeTime Project
 * @package     caketime
 * @subpackage  caketime.views.helpers
 * @author      Graham Weldon (http://grahamweldon.com)
 * @license     http://www.opensource.org/licenses/mit-license.php The MIT License
 */
class AutoJavascriptHelper extends AppHelper {

/**
 * Options
 *
 * path => Path from which the controller/action file path will be built
 *         from. This is relative to the 'WWW_ROOT/js' directory
 *
 * @var array
 * @access private
 */
    
private $__options = array('path' => 'autoload');

/**
 * View helpers required by this helper
 *
 * @var array
 * @access public
 */
    
public $helpers = array('Javascript');

/**
 * Object constructor
 *
 * Allows passing in options to change class behavior
 *
 * @param string $options Key value array of options
 * @access public
 */
    
public function __construct($options = array()) {
        
$this->__options array_merge($this->__options$options);
    }

/**
 * Before Render callback
 *
 * @return void
 * @access public
 */
    
public function beforeRender() {
        
extract($this->__options);
        if (!empty(
$path)) {
            
$path .= DS;
        }

        
$files = array(
            
$this->params['controller'] . '.js',
            
$this->params['controller'] . DS $this->params['action'] . '.js');

        foreach (
$files as $file) {
            
$file $path $file;
            
$includeFile WWW_ROOT 'js' DS $file;
            if (
file_exists($includeFile)) {
                
$file str_replace('\\''/'$file);
                
$this->Javascript->link($filefalse);
            }
        }
    }
}

?>

A small disclaimer is that this helper is very basic. There are probably some performance considerations to make when checking the disk for file existence on every single request. However, the solution is elegant and unobtrusive. Questions / comments and suggestions are encouraged.

Comments

  • Posted 02/02/11 08:50:09 AM
    I just upgraded from 1.3.6 to 1.3.7 and the auto js helper threw some errors.

        public function __construct($options) { 
            if(empty($options)) { $options = array(); }
            $this->__options = array_merge($this->__options, $options); 
        } 

    I don't pass any additional options, so this works for me and should still work with options passed.
  • Posted 09/14/10 07:29:58 AM
    I expected that for controller "MySuperBakesController" which is defined in file "my_super_bakes_controller.php" and has function "index()" name for 'js' file which will be automatically loaded will be "WEBROOT/js/autoload/my_super_bakes/index.js" but unfortunately it is "WEBROOT/js/autoload/MySuperBakes/index.js" If one wants to have "automatic include" structure and naming like in "views" should add "Inflector::underscore()" for every controller name: "Inflector::underscore($this->params['controller'])" in place of "$this->params['controller']"
  • Posted 07/13/10 07:15:21 AM
    This is really nice that we can add js according to controllers.

    But i wanted to add particular js to all controller, then how to add?
    please update me. I am getting stuck into this. Its really hard to add js in all views n that is not correct way..

    Waiting for reply..
    • Posted 07/13/10 09:55:48 AM
      This is really nice that we can add js according to controllers.

      But i wanted to add particular js to all controller, then how to add?
      please update me. I am getting stuck into this. Its really hard to add js in all views n that is not correct way..

      Waiting for reply..

      Hey.

      If you want to add JS that is loaded for *all* controllers, then you just need to use the CakePHP standard javascript include, or a script tag.

      $this->Html->script('myscript'); // This is for CakePHP 1.3

      $javascript->link('myscript'); // This is for CakePHP 1.2

      This will load for all controllers, if placed in your layout.
  • Posted 05/21/10 09:09:39 AM
    Updated code for CakePHP 1.3
  • Posted 05/21/10 06:40:03 AM
    Thank you for the helper :)

    I had to do a string replace to get the generated js links working.

    By default, I get

    src="/cakephp/myapp/js/autoload\controller.js"

    (the separator is \ which results in an "Object not found error")

    str_replace was done to the \helpers\auto_javascript.php

    $file = str_replace('\\', '/', $file);
    $this->Javascript->link($file, false);

    Thank you
  • Posted 08/31/09 06:11:50 PM
    Well, the entire idea of using autoload is *not* loading things manually, which is what I am doing right now anyway :-)

    That said, I've spent a few hours investigating how the callbacks are called (when, under what conditions, in what sequence, etc.). I'm still puzzled as to how could I determine the plugin (if any) *inside* the auto javascript helper code. As far as I could understand the information isn't there. I'll keep looking though.
  • Posted 08/30/09 06:11:02 PM
    I'm pretty new to CakePHP, having studied in depth it since last week. One of the things that caught my attention as I was working with a pretty sizeable project was the amount of repeated Javascript code. Things that could and should be modularized were duplicated in several places, partly due to the way CakePHP works. For one, it's not obvious where should I put all the code, specially code that I want to load only on some views. Your plugin goes a long way solving this problem. However, I found one special case that I have to solve if I want to use it.

    I have one controller named Projects, which is subclassed into special project types. The descendant classes are implemented as plugins (note: I didn't write this part, just inherited from the previous developer, but I assume that's a reasonable way to do it). The problem is that I have no way to load specific js files for each plugin.

    For the curious: the elements are rendered with the following call at the end of each view in the base model:
    $this->renderElement()

    Is anyone aware of a solution for this case? My best guess right now is to call the autoloader manually before my call to renderElement(), and put the js files inside the plugin folder. Is that ok?
  • Posted 08/29/09 02:04:53 PM
    I've always nameed javascript files after the controller/views, but manually included them - why didn't I think of this? However, whilst reading this, it struck me: Why doesn't Cake apply the same conventions to javascript as it does to models/views/controllers? If the files exist, then the following are automatically included: app.js, [controller_name].js and finally [action].js. I can't imagine the performance hit would be that great...

    Should fail fast for non-html output only though...
  • Posted 08/14/09 09:29:43 AM
    Thanks Oscar for those notes. You've identified an issue in using the File class in this manner.

    I have replaced file checking with file_exists() and have refactored the beforeRender to loop over a defined array of files, which means adding more files in the future means less additional code.

    Finally, I altered the path option to allow you guys to optionally set it to false or empty, which will allow automatic inclusion to happen directly from the webroot/js/ directory.
    So, if you set path to '', you don't need to create the additional 'autoload' directory.

    Thanks for the great comments and feedback from everyone! Very much appreciated!
  • Posted 08/14/09 08:39:27 AM
    Upong further investigation, the File class isn't really suitable to use for this, as it doesn't handle non-existant files very well (at least not with open_basedir).

    Doing a simple file_exists() instead will be a lot better IMO.
  • Posted 08/14/09 07:02:02 AM
    @Mark
    Not sure.

    @Graham
    I've found a little problem. If the folder doesn't exist, for example "js/views/pages" when it tries to look for the file "js/views/pages/home.js", $this->File->path points to "/home.js" instead, triggering a open_basedir error..

    Is there any way to solve that without creating empty folders?
  • Posted 08/12/09 03:48:02 AM
    yet so brilliant, great job. I will include this helper in my project :D
  • Posted 08/11/09 03:46:45 PM
    Very nice !

    If we have the same functions in add.js edit.js, what is the best way to avoid duplicate the source code ?

    • Posted 08/11/09 07:39:18 PM
      If we have the same functions in add.js edit.js, what is the best way to avoid duplicate the source code ?
      If they are functions, you can define them at the controller level, and just call them in the action level JS.

      /webroot/js/autoload/users.js => define functions

      /webroot/js/autoload/users/add.js => Call defined functions
      /webroot/js/autoload/users/edit.js => Call defined functions

      Controller level JS is included before action JS, so you should be good to go with that.
      • Posted 08/12/09 02:23:04 PM
        If they are functions, you can define them at the controller level, ....
        Nice, thanks
  • Posted 08/11/09 09:29:03 AM
    @oscar
    i thought php caches those file_exists() only for the duration of the request (if called several times in it).
    does it actually cache it longer than that?
  • Posted 08/11/09 07:22:51 AM
    Very nice, will try this right away!

    file_exists is cached by PHP, so there shouldn't be any performance hit.

    Is it possible to extend this code to include JS for elements?

Comments are closed for articles over a year old