Hi All,
I’ve been pondering over an idea for using Yii as AJAX framework which could simplify otherwise complex page refreshes. Consider situations where multiple sections on the page might be modified by a user’s action, such-as logging in. Generally, the done thing is to process any input and redirect the user back to the page they were viewing. This is OK, it’s generally what we’ll need to do if the user has JavaScript disabled, but it’s not really optimal. Using renderPartial and jQuery’s load() function we can update part of the page, but this can only really do one part of the page and usually requires some extra logic.
Well, my idea is to extend CController and CActiveRecord, modifying the functionality of render and renderPartial such that they are aware of what attributes have been changed. Firstly, however, you’d want to override the __set and __get of each model so that you can record which properties have changed and which have been accessed.
Some rough code:
protected $_Modified = array();
public function __set($name,$value)
{
$this->_Modified[$name] = true;
parent::__set($name,$value);
}
public function __get($name)
{
if($this->_Modified[$name] &&
property_exists(Yii::app()->controller,'accessedModifiedData'))
Yii::app()->controller->accessedModifiedData = true;
return parent::__get($name);
}
Pretty simple so-far. Now for extending the controller:
public $accessedModifiedData = false;
public $AjaxOutput = array();
public function renderPartial($view, $data=NULL, $return=false, $processOutput=false)
{
$inModified = $this->accessedModifiedData;
$output = parent::renderPartial($view, $data, true, $processOutput);
if(!$inModified && $this->accessedModifiedData && $data['containerId']) {
$AjaxOutput[$data['containerId']] = $output;
$this->accessedModifiedData = false;
}
if($return) return $output;
else echo $output;
}
public function render($view, $data=NULL, $return=false)
{
$output = parent::render($view, $data, true);
if(Yii::app()->request->getIsAjaxRequest()) {
if($return) return CJSON::encode($this->AjaxOutput);
else echo CJSON::encode($this->AjaxOutput);
} else {
if($return) return $output;
else echo $output;
}
}
Again, pretty simple. One addition you’ll notice in renderPartial is that the data item ‘containerId’ has special meaning. For reasons that should become clear soon, this should be the HTML ID of the container which contains the renderPartial HTML. If a modified property is accessed within a sub-view, the HTML of the view is added to the AjaxOutput associative array with containerId as as the key.
The render function is then modified so that, if the request is an AJAX request, the AjaxOutput array is returned as JSON, which brings us to the Javascript side of things. Handle your AJAX requests as normal, simply ensure it’s set-up to parse the response as JSON and use the following as your success function:
updateContainers(json)
{
for(var i in json) {
$('#'+i).html(json[i]);
}
}
Hey presto. Each item that changed on the page has been updated. There are some tweaks that can be made in the name of robustness/compatibility (if a list page has a new item added, for example, the container for it won’t exist yet) but you get the idea. Simply overriding the behaviour of render() on any ajax request might not fit with every webapp, a separate renderAjaxable function might be called for, I just wanted to show updates being done with a minimal amount of re-coding.
So… What do people think? Is it cookbook-able?