Automatic Javascript Includer Helper

By Graham Weldon (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:

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

Here is the helper code:


Helper Class:

Download code <?php 
/** File: auto_javascript.php **/
/**
 * Auto JavaScript Helper
 *
 * Facilitates JavaScript Automatic loading and inclusion for page specific JS
 *
 * @copyright   Copyright 2009, Graham Weldon
 * @author      Graham Weldon
 * @link        http://grahamweldon.com
 * @version     0.1
 * @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 am($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)) {
                
$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 1180

CakePHP Team Comments Author Comments
 

Comment

1 Nice helper!

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?
Posted Aug 11, 2009 by Oscar Carlsson
 

Comment

2 cached?

@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 Aug 11, 2009 by Mark
 

Question

3 Duplicate code ...

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 Aug 11, 2009 by fpalmer
 

Comment

4 Duplicate Code organisation

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 Aug 11, 2009 by Graham Weldon
 

Comment

5 so simple

yet so brilliant, great job. I will include this helper in my project :D
Posted Aug 12, 2009 by Tomasz Wójcik
 

Comment

6 thanks

If they are functions, you can define them at the controller level, ....
Nice, thanks
Posted Aug 12, 2009 by fpalmer
 

Comment

7 open_basedir error when folder is missing

@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 Aug 14, 2009 by Oscar Carlsson
 

Comment

8 Don't use the File class

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 Aug 14, 2009 by Oscar Carlsson
 

Comment

9 Updates

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 Aug 14, 2009 by Graham Weldon
 

Comment

10 A very useful addition to the tool box

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 Aug 29, 2009 by GreyCells
 

Comment

11 How can I deal with elements inside plugins?

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 Aug 30, 2009 by Carlos Ribeiro
 

Comment

12 @Carlos: Can you just load it manually?

Posted Aug 31, 2009 by Nick Baker
 

Comment

13 @Nick Baker That was not the point...

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 Aug 31, 2009 by Carlos Ribeiro