NiceHead helper with autoloading of javascript and css

By Kim Biesbjerg (biesbjerg)
Like cake isn't already doing enough magic...
Here is a helper to make it even better! Injects CSS/JS into the head tag in your layout and autoloads css/js files for you based on current controller and action.

The setup


Credits:

Kudos to http://rossoft.wordpress.com and jirikupiainen.com for inspiration!

Include NiceHead in your helpers array like this:

Download code
<?php
class TestsController extends AppController {

    var 
$name 'Tests';
    var 
$helpers = array('NiceHead');
    
    function 
admin_index()
    { .....

Put this in your layout between the head tags :

Download code
<head>
...
<?php if(isset($niceHead)) $niceHead->flush();?>
...
</head>

Now you can do this something like this in your views:


Download code
<?php
// Inject css file in head
$niceHead->css('css_file');

// Inject javascript file in head
$niceHead->js('js_file');

// Inject javascript block in head
$niceHead->jsBlock('alert("hello world!");');

// Inject css block
$niceHead->cssBlock('.class{ background:blue; }');

// Inject Raw code
$niceHead->cssBlock('I dont know what this text is doing in the head!');
?>

Get funky with prototype


Additionally I included some methods that requires prototype and Dan Webb's DomReady.

Get prototype: http://www.prototypejs.org
Get onDOMReady.js: http://smoothoperatah.com/files/onDOMReady.js
Put the files in webroot/js and include them in your head before doing $niceHead->flush(); :

Download code
<?php e($javascript->link('prototype''onDOMReady'));?>


Now you should be able to use the extra methods:

Download code
<?php
// Inject javascript block in head to load on window load
$niceHead->jsOnLoad('alert("This will execute when all contents has loaded');

// Inject javascript block in head to load on DOM ready
$niceHead->jsOnReady('alert("This will execute when DOM has loaded');
?>

Autoload magic


By default NiceHead will try to autoload JS and CSS for you.

Download code
For this url: www.domain.com/admin/users/login

NiceHead will first check if these two files exists:

Download code
webroot/themed/current_theme/css/users/users.css
webroot/themed/current_theme/css/users/users_admin_login.css

If not it'll look for:

Download code
webroot/css/users/users.css
webroot/css/users/users_admin_login.css

If files exist in any of above places it will auto inject them into the head of the layout.

It does exactly the same for javascript files.

If you for some reason don't want this feature it can be disabled in the helper:

Download code
class NiceHeadHelper extends Helper
{
    /**
     * Autoload configuration
     * 
     * Put files in your CSS/JS
     * /app/webroot/css|js/controller/controller.css|controller_action.css
     * /app/webroot/themed/theme/css|js/controller/controller.css|controller_action.css
     * 
     */
    var $autoloadCss = true;
    var $autoloadJs = true;
......


That's all folks!


Now, before I forget, here is the actual helper:

Download code
<?php
/**
 *    NiceHead helper
 *    @author Kim Biesbjerg
 *     @desc     This helper can inject CSS/JS into the head of your layout
 *             and autoload CSS/JS based on current controller/action
 * 
 *             Requires PrototypeJS and Dan Webb's DomReady to function properly.
 *             Prototype: www.prototypejs.org
 *             DomReady: http://smoothoperatah.com/files/onDOMReady.js
 *     @version 19. april, 2007 
 */
class NiceHeadHelper extends Helper
{
    
/**
     * Autoload configuration
     * 
     * Put files in your CSS/JS
     * /app/webroot/css|js/controller/controller.css|controller_action.css
     * /app/webroot/themed/theme/css|js/controller/controller.css|controller_action.css
     * 
     */
    
var $autoloadCss true;
    var 
$autoloadJs true;
    
    
/**
     * We use Cake's own Html/Javascript helpers
     * to generate tags to wrap around registered items
     *
     * @var array
     */
    
var $helpers = array('Html''Javascript');

    
/**
     * Order to flush registered items in <head>
     *
     * @var array
     */
    
var $priority = array('js''css''jsOnReady''jsOnLoad''jsBlock''cssBlock''raw');
    
    
/**
     * Holds our registered items
     *
     * @var array
     */
    
var $_registered = array();
    
    function 
__construct()
    {
           static 
$library = array();
           
$this->_registered =& $library;
    }

    function 
beforeRender()
    {
        
$this->_autoload();
    }
    
    
/**
     * Function to check if file exists and autoload
     * if $autloadCss/$autoloadJs is set to true
     */
    
function _autoload()
    {
        
/**
         * Get current controller and action
         */
        
$controller $this->params['controller'];
        
$action $this->params['action'];
        
        
/**
         * Check if we are supposed to autoload controller/action css
         */
        
if($this->autoloadCss)
        {
            
/**
             * CSS base paths
             */
            
$themedCssPath WWW_ROOT $this->themeWeb CSS_URL $controller DS;
            
$commonCssPath WWW_ROOT CSS_URL $controller DS;

            
/**
             * Check if CSS file for current controller exists
             */
            
if(file_exists($themedCssPath $controller '.css') || file_exists($commonCssPath $controller '.css'))
            {
                
$this->css($controller DS $controller);
            }
            
            
/**
             * Check if CSS file for current action exists
             */
            
if(file_exists($themedCssPath $controller '_' $action '.css') || file_exists($commonCssPath $controller '_' $action '.css'))
            {
                
$this->css($controller DS $controller '_' $action);
            }
        }
        
        
/**
         * Check if we are supposed to autoload controller/action js
         */
        
if($this->autoloadJs)
        {        
            
/**
             * JS base paths
             */
            
$themedJSPath WWW_ROOT $this->themeWeb JS_URL $controller DS;
            
$commonJSPath WWW_ROOT JS_URL $controller DS;
            
            
/**
             * Check if JS file for current controller exists
             */
            
if(file_exists($themedJSPath $controller '.JS') || file_exists($commonJSPath $controller '.JS'))
            {
                
$this->js($controller DS $controller);
            }
            
            
/**
             * Check if JS file for current action exists
             */
            
if(file_exists($themedJSPath $controller '_' $action '.js') || file_exists($commonJSPath $controller '_' $action '.js'))
            {
                
$this->js($controller DS $controller '_' $action);
            }
        }
    }
    
    
/**
     * Includes a block of javascript on dom load
     *
     * @param string $input
     */
    
function jsOnReady($input)
    {
        
$this->_register($input'jsOnReady');
    }
    
    
/**
     * Includes a block of javascript on window load
     *
     * @param string $input
     */
    
function jsOnLoad($input)
    {
        
$this->_register($input'jsOnLoad');
    }
    
    
/**
     * Includes an external javascript file
     *
     * @param string $input
     */
    
function js($input)
    {
        
$this->_register($input'js');
    }
    
    
/**
     * Includes a block of javascript
     *
     * @param string $input
     */
    
function jsBlock($input)
    {
        
$this->_register($input'jsBlock');
    }
    
    
/**
     * Includes an external stylesheet
     *
     * @param string $input
     */
    
function css($input)
    {
        
$this->_register($input'css');
    }
    
    
/**
     * Includes a block of styles
     *
     * @param string $input
     */
    
function cssBlock($input)
    {
        
$this->_register($input'cssBlock');
    }
    
    function 
raw($input)
    {
        
$this->_register($input'raw');
    }
    
    
/**
     * Internal function used to register items
     *
     * @param string $item
     * @param string $type
     */
    
function _register($item$type)
    {
        if(!
array_key_exists($type$this->_registered))
        {
            
$this->_registered[$type] = array();
        }
        
        if(!
in_array($item$this->_registered[$type]))
        {
            
$this->_registered[$type][] = $item;
        }                   
    }                                          

    
/**
     * Output the registered items
     *
     */
    
function flush()
    {
        foreach(
$this->priority as $type)
        {
            if(
array_key_exists($type$this->_registered))
            {
                
$items $this->_registered[$type];
                
                switch(
$type)
                {
                    case 
'css':
                        foreach(
$items as $item)
                        {
                            
e($this->Html->css($item));
                        }
                        break;
                    case 
'js':
                        foreach(
$items as $item)
                        {
                            
e($this->Javascript->link($item));
                        }
                        break;
                    case 
'raw':
                        foreach(
$items as $item)
                        {
                            
e($item);
                        }
                        break;                    
                    case 
'jsOnReady':
                        
$output  "Event.onDOMReady(function(){";
                        
$output .= join($items);
                        
$output .= "});";
                        
e($this->Javascript->codeBlock($output));
                        break;
                    case 
'jsOnLoad':
                        
$output  "Event.observe(window, 'load', function(){";
                        
$output .= join($items);
                        
$output .= "});";
                        
e($this->Javascript->codeBlock($output));
                        break;
                    case 
'jsBlock':
                        
$output join($items);
                        
e($this->Javascript->codeBlock($output));
                        break;
                    case 
'cssBlock':
                        
$output join($items);
                        
e($this->Html->css($output));
                        break;
                    default:
                        die(
"Internal error. Unknown type: '{$type}'");
                }                    
            }
            
        }
    }
}
?>


Cheers, biesbjerg

 

Comments 349

CakePHP Team Comments Author Comments
 

Comment

1 Sweet

nice helper :D is going to help alot :D
Posted May 18, 2007 by Henrik Bjornskov
 

Bug

2 Good stuff

This is great, thanks for putting it up, and a well written tutorial to boot.
One thing, the cssBlock case at the end of the helper spits out a link to a css file:

case 'cssBlock':
$output = join($items);
e($this->Html->css($output));

Rather than inline css:

case 'cssBlock':
$output = join($items);
e('< style type="text/css" >' . $output . '');
Posted May 18, 2007 by Peter Baker
 

Comment

3 Origin of JS and CSS autoloading

Just for reference, here is the original helper for JS and CSS autoloading.

Please also note that built-in CakePHP helpers provide a lot of the same functionality - namely JavascriptHelper::link() and HtmlHelper:css()
Posted May 24, 2007 by Jiri Kupiainen
 

Comment

4 Great Helper with a Suggestion

What a great find. I had just coded a similar helper when I found yours. I really like the way it uses stacks. It's a nice solution when you need to add head tags to a layout.

Works great with one exception. If you use NiceHead to output tags in both your layout and your view, the view tags are pushed onto the array before the layout tags. That's because the view is processed before the layout.

When you have css files that overload selectors, you loose that overloading since the base css in the layout is rendered last.

My solution was to add an additional parameter to the css() and _register() methods. If set to true, the tag is pushed onto the front of the list using array_unshift(). Here's how I modified nice_head.php to make it work:

<?php
//...
    /**
     * Includes an external stylesheet
     *
     * @param string $input
     */
    
function css($input$push false)
    {
        
$this->_register($input'css'$push);
    }
//...
    /**
     * Internal function used to register items
     *
     * @param string $item
     * @param string $type
     */
    
function _register($item$type$push false)
    {
        if(!
array_key_exists($type$this->_registered))
        {
            
$this->_registered[$type] = array();
        }
        
        if(!
in_array($item$this->_registered[$type]))
        {
            if (
$push === true)
                
array_unshift($this->_registered[$type], $item);
            else
                
$this->_registered[$type][] = $item;
        }                   
    }                                          
//...
?>

In the layout:

<?php if(isset($niceHead)) $niceHead->css('overloadingLayoutCss'true); ?>
<?php 
if(isset($niceHead)) $niceHead->css('baseLayoutCss'true); ?>

In the view:

<?php if(isset($niceHead)) $niceHead->css('baseViewCss'); ?>
<?php 
if(isset($niceHead)) $niceHead->css('overloadingViewCss'); ?>

The only caveat with that method is if you have multiple overloading css links in the layout, you have to add them in reverse order. Other that that, it works great!

Might be a good candidate for inclusion in the core.
Posted Aug 30, 2007 by Al PropNut