Wednesday, February 29, 2012

Leveraging Widgets, the Widget Factory and Skins to make DRY code in themed sites

From the most basic of sites to the most complex, Yii offers us a way to consistently format the styles and data we present across multiple pages and even multiple sites easily though the use of the CWidgetFactory and skins. 

I have used and created numerous CWidget based components, and often use the CWidgetFactory for adding consistency, but it was only today that I finally delved into skins for Widgets and the capabilities that they provide. I could never quite wrap my head around all the different pieces of how to fit it into a site or multiple sites. When I did, it was game changing.

Full information about the widget factory can be found in the Yii API documentation here: http://www.yiiframework.com/doc/api/1.1/CWidgetFactory

I will share an example of how to progress from a simple set of similar views and adjust those views step by step until we have simplified them to the point that they can be themed without the need to replace the view file, or in replacing the view file, negate the need to specify all the parameters for the widget contained in that view directly (thereby reducing the chance that a crucial change will not be propagated from one theme to the next).

I should probably have broken this down into a series of posts, but I wanted to be sure that I actually got around to posting it all, and this seemed the best way. If some of the later portions are confusing, set it aside and come back to it later once the basic idea of using the Widget Factory has sunk in.


Starting with a common view to display details of a model.
<div id=”myDisplay” class=”greyBackground”>
<?php
    $this->widget(‘zii.widgets.CDetailView’, array(
        ‘model’=>$model,
        ‘htmlOptions’=>array(
            ‘class’=>’boxedDetail zebra-striped’,
        ),
        ‘template’=>’<span>{label}</span><span>{value}</span>’,
        ‘attributes’=>array(
            ‘id’,
            ‘name’,
            ‘url’,
        )
    ));
?>
</div>

In a second page, we have similar information displayed for a different type of model. At the outset, we could simply configure the same sort of detail for the other view, but with different attributes:

<div id=”myOtherDisplay” class=”greyBackground”>
<?php
    $this->widget(‘zii.widgets.CDetailView’, array(
        ‘model’=>$otherModel,
        ‘htmlOptions’=>array(
            ‘class’=>’boxedDetail zebra-striped’,
        ),
        ‘template’=>’<span>{label}</span><span>{value}</span>’,
        ‘attributes’=>array(
            ‘id’,
            ‘comment’,
            ‘lastUpdate’,
        )
    ));
?>
</div>

That’s a lot of repetition.
We can simplify things immensely by turning the WidgetFactory on and configuring all of the CDetailView widgets to have the same basic properties. We do this by setting it up in the protected/config/main.php file:
…
‘components’=>array(
    …
    ‘widgetFactory’=>array(
        ‘class’=>’CWidgetFactory’,
        ‘widgets’=>array(
            ‘CDetailView’=>array(
                ‘htmlOptions’=>array(
                    ‘class’=>’boxedDetail zebra-striped’,
                ),
               ‘template’=>’<span>{label}</span><span>{value}</span>’
            ),
        ),
    )
    …
)
Now, our views can be simplified:
<div id=”myDisplay” class=”greyBackground”>
<?php
    $this->widget(‘zii.widgets.CDetailView’, array(
        ‘model’=>$model,
        ‘attributes’=>array(
            ‘id’,
            ‘name’,
            ‘url’,
        )
    ));
?>
</div>
Not a huge improvement in this one view, but for situations where the same format and classes are desired on many pages throughout a site, it’s a tremendous consistency aid to make sure that all similar components throughout a site will, by default, have the same properties. Multiply the removal of those 4 lines times the number of CDetailView widgets used throughout the site, and you can begin to see the benefit therein.

If a particular CDetailView needs to be different, simply specifying the attribute where the widget is invoked will cause that property to take precedence over what is specified in the widget factory.

But, do you really want to put a lot of html class settings and options into your main config file? What if you have a workflow that requires people to set up the display aspects, but those people should not have access to the main config with it’s information about SQL passwords etc.

Hello SKINS!

We can pull those configuration arrays for the widget factory OUT of the main config file, and put them into mini-configuration files specific to the widgets.

Before we do that, we need to enable skinning in the widgetFactory component for the site. Update the config file so that ‘enableSkin’=>true in your widgetFactory array.

Then, within the protected/views/ folder or the theme/{themename}/views/ folder, create a ‘skins’ folder.
If you have a theme set up, it will look there first. Failing to find the file it’s looking for, it will fall back. Or you can specify an alternate location for your skins directory (http://www.yiiframework.com/doc/api/1.1/CWidgetFactory#skinPath-detail)

So how do we skin it?


To skin a widget, create a file with the name of the widget in the skins folder, i.e.,:
/protected/views/skins/CDetailView.php
<?php
return array(
    ‘default’=>array(
        ‘htmlOptions’=>array(
            ‘class’=>’boxedDetail zebra-striped’,
        ),
        ‘template’=>’<span>{label}</span><span>{value}</span>’,
    ),
    ‘nostripes’=>array(
        ‘htmlOptions’=>array(
            ‘class’=>’boxedDetail’,
        ),
        ‘template’=>’<span>{label}</span><span>{value}</span>’,
    )
);
?>
The key value of the array is the identifier for the skin. All you really need, is the ‘default’ entry.

And then the main config file can simply declare the widgetFactory component as such:
‘components’=>array(
    …
    ‘widgetFactory’=>array(
        ‘class’=>’CWidgetFactory’,
        ‘enableSkin’=>true,
    )
    …
),
Want to switch all the CDetailView widgets to using the unstriped version, but not switch your default setting? Add the CDetailView array back into the widgetFactory component, but this time, rather than specifying all of the column values, only specify the skin property as ‘nostripes’
‘components’=>array(
    ‘widgetFactory’=>array(
        ‘enableSkin’=>true,
        ‘class’=>’CWidgetFactory’,
        ‘widgets‘=>array(
            ‘CDetailView’=>array(
                ‘skin’=>’nostripes’,
            )
        )
    )
)

We’ve now made it VERY easy for someone with a minimum knowledge of the overall system to change just the look and feel of the site if needed by simply selecting the skin components that they want to use.

If we’re using themes, simply putting different skin configuration files into the theme view folders will enable you to completely change the configuration per-theme.

Everything in this example was very straight forward and simplistic.

Imagine how powerful it can be if you take it a step in a slightly different direction:

Lets assume PostGridView is a class which extends the CGridView Widget. This enables me to provide some basic options at the class level and isolate any widget factory skinning to just the sub-class that I'm attempting to affect.

/protected/views/skins/PostGridView.php
<?php

return array(
    ‘default’=>array(
        ‘htmlOptions’=>array(‘class’=>’post-grid-view’),
        ‘columns’=>array(
            ‘id’,
            ‘title’,
            ‘description:text:My Description’,
            array(
                ‘class’=>’CButtonColumn’,
                ‘template’=>’{view}{update}’
            )
        ),
    ),
    ‘minimal’=>array(
        ‘htmlOptions’=>array(‘class’=>’post-grid-view minimal’),
        ‘columns’=>array(    
            ‘title’,
            ‘description:truncatedText:Desc.’,
        ),
    ),
);

?>
 
So then, in one view I may:
<p>some complex themed information here</p>
<?php
    $this->widget(‘PostGridView’); 
?>
<p>Other themed information here.</p>
In another:
<div id=”sidebar widget”>
<?php
        $this->widget(‘PostGridView’, array(‘skin’=>’minimal’));
?>
</div>
And, if I need to create another theme version of the view, I can do so without having to recreate all the complex logic of the columns required:
<fieldset id=”sidebar widget”>
    Added HTML content or other dynamic information here that only
    shows up in this theme view…
    <?php
        $this->widget(‘PostGridView’, array(‘skin’=>’minimal’));
    ?>
</fieldset>
Which is MUCH more efficient than having to specify the full column declaration in each themed version of the view.

If your site is simple, you will probably never need to progress beyond the basic widgetFactory, if even that far.

But, if the site utilizes many commonly shared files or multiple theme versions of the same views, then leveraging the skins capability of the widget factory becomes imperative if you wish to avoid repeating code and avoiding potential oversights when making changes to something that should be applied site-wide or across sites.

Combine in some custom formatters (note the use of the truncateText format option in the minimal config) and you’ve got some real power lifting the application without having to put anything but the most basic logic and presentation into the view or adding formatting logic to magic method properties of your models.