Automatic Javascript Includer Helper
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
- users
- autoload
- 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($file, false);
}
}
}
}
?>
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
Comment
1 Nice helper!
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?
Comment
2 cached?
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?
Question
3 Duplicate code ...
If we have the same functions in add.js edit.js, what is the best way to avoid duplicate the source code ?
Comment
4 Duplicate Code organisation
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.
Comment
5 so simple
Comment
6 thanks
Nice, thanks
Comment
7 open_basedir error when folder is missing
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?
Comment
8 Don't use the File class
Doing a simple file_exists() instead will be a lot better IMO.
Comment
9 Updates
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!
Comment
10 A very useful addition to the tool box
Should fail fast for non-html output only though...
Comment
11 How can I deal with elements inside plugins?
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?
Comment
12 @Carlos: Can you just load it manually?
http://book.cakephp.org/view/742/Plugin-Images-CSS-and-Javascript
http://book.cakephp.org/view/562/Requesting-Elements-from-a-Plugin
Comment
13 @Nick Baker That was not the point...
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.
Comment
14 Nice Post.