Ajax update framework

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?

You can make your own myRender method to a class extending CController (eg: Controller), then use that method to do the render thing in your application.

Also, the changes to __get and __set should be done in clasess extending CActiveRecord (eg.: ActiveRecord)

Then you simpley put these clases under components folder (for example) and extend your active record classes from ActiverRecord, and your controllers form Controller (this is already done if you make your skelleton application with yiic webapp)

I don’t think this change should be done in the core, because is (I think) a special need…

Yes, I wasn’t meaning for it to be a core change. I myself did override render() in Controller, as opposed to adding a myRender function, but I also made my AJAX calls add another variable, getJSON, to the query string so I knew for sure that a JSON response was expected.

As for simplifying complex AJAX updates being a special need, perhaps. Nowadays AJAX updates are seen as professional, enhancing the browsing experience, but I’ve also had clients and jobs that required support for people browsing with JavaScript disabled. The way I see it, it’s like using Yii’s secure cookies. It’s is not necessary for the site to function, but the site is better-off for it being there.

True, multiple updates on the same page when a model is updated might be on the specialist end of the spectrum, but I’m presently thinking on how best to get this and the jQuery bbq plugin (http://benalman.com/projects/jquery-bbq-plugin/) to work in harmony with yii. Seamless integration of AJAX with no code changes strikes me as being highly desirable, as it also means everything works as it did before when JavaScript is disabled.

What I mean by making your own myRender method is becouse that method will be used whenever you want, more if, as you already do, you add a variable to all the ajax calls.

For example




function actionXXX(){ // this action belongs to some controller extending Controller.

 		// do your stuff

    	if(isset($_GET['getJSON'])) {

   		// use myRender method

    	} else { 

        	//use normal renders methods (render() or renderPartial() may be you need another if to coohse which one)

    	}


}



I see, but how would the functionality of myRender differ from simply overriding render? I could potentially save myself the full expense of a render call by only calling renderPartial on my view, but that only works if the content I need to update is in my rendered view. If the content that must be updated is outwith my action’s view (a set of user controls in the header, for example, which alternates between register/login and welcome/logout), I will need to render everything anyway to be sure.

I’m presently working on a complex e-commerce/CMS system. As the majority of the tables in the database feature CRUD functionality or more, I’d probably be writing/copying the if(isset($_GET[‘getJSON’])) statement over 100 times by placing it in every action. Even for a smaller project, I’d choose to override the render() method of my Controller class (CController extension) simply to avoid the repetition.

It is perfect to override any method of Yii. Yii IS extensible.

I’m only talk of an alternative, not a final solution.

But sometimes is better to have the things simple as they are and add new functionality, this also belongs to the "extensibility" part of Yii.

The idea is very nice, but you’re still rendering the whole page? I don’t see the advantage then?