I’d like to share a code snippet which turned out to be quite helpful for me. You might know this from your own experience: You are writing a comment or email in a browser form and just before finishing your “essay” the brower crashes or some other minor catastrophe occurs and all your work of the last 30 minutes is lost!
Then, gmail came along and offered the "autosave" functionality which saves your draft every minute or so.
Well, I needed something like this for my work as well. I would like to share it with you since once it’s implemented, it can be used in every ActiveForm of your Yii app.
There are a few steps involved.
- Download the jquery timer plugin (I used v. 1.2): http://plugins.jquery.com/node/3656/release
(I hope this is the link, right now the jquery plugin page is offline and shows some maintainance message.)
-
Extend CActiveForm
-
Extend CController
-
Extend CActiveRecord
-
Put everything together
1. Download jquery timer plugin
see above
2. Extend CActiveForm
(It’s generally good practice to extend important classes such as CActiveRecord, CActiveForm, CController etc. to allow for project-wide adjustments).
First, we need a new flag to allow a form to enable auto-save (similar to "enableAjaxValidation")
In your components folder, create a file called "ActiveForm.php". In there, you extend CActiveForm and add the flag for autosave and override the textArea and textField methods to account for autosave (this is just done by adding a class "auto-save" to the text field or area).
class ActiveForm extends CActiveForm
{
public $enableAutoSave = false;
public function textArea ($model, $attribute, $htmlOptions=array ())
{
if ($this->enableAutoSave)
{
if (isset ($htmlOptions['class']))
$htmlOptions['class'] += ' auto-save';
else
$htmlOptions['class'] = 'auto-save';
}
return parent::textArea ($model, $attribute, $htmlOptions);
}
public function textField ($model, $attribute, $htmlOptions=array ())
{
if ($this->enableAutoSave)
{
if (isset ($htmlOptions['class']))
$htmlOptions['class'] += ' auto-save';
else
$htmlOptions['class'] = 'auto-save';
}
return parent::textField ($model, $attribute, $htmlOptions);
}
...
next, you need to override CActiveForm’s “run” method in your ActiveForm class. It needs to add the jquery code which triggers the autosave functionality:
... public function run ()
{
parent::run ();
if ($this->enableAutoSave)
{
$cs = Yii::app()->clientScript;
$baseUrl=Yii::app()->getBaseUrl ();
$cs->registerScriptFile($baseUrl.'/js/jquery.timers-1.2.js');
$cs->registerScript('ActiveForm#enableAutoSave',"
var autosave = function (i){
".CHtml::ajax(array(
'url'=>CHtml::normalizeUrl(''),
'type'=>'post',
'dataType'=>'json',
'data'=>'js:$(\'#'.$this->id.' .auto-save\').serialize()+\'&autosave='.$this->id.'\'',
'success'=>'js:function(data){
$(\'#'.$this->id.' div.auto-save-info span\').parent().css(\'visibility\', \'visible\');
if (data.success)
{
$(\'#'.$this->id.' div.auto-save-info span\').html(data.time);
}
else
{
$(\'#'.$this->id.' div.auto-save-info span\').html(\''.Yii::t('global','Autosave failed!').'\');
}
}',
))."
};
var autosave_sync = function (){
".CHtml::ajax(array(
'url'=>CHtml::normalizeUrl(''),
'async'=>false,
'type'=>'post',
'dataType'=>'json',
'data'=>'js:$(\'#'.$this->id.' .auto-save\').serialize()+\'&autosave='.$this->id.'\'',
))."
};
$('#".$this->id."').everyTime(120000, autosave);
$(window).unload (autosave_sync);
");
}
}
This collects all the text fields and areas of an ActiveForm which have the auto-save class set and sends the data off to your server. Note that the ajax function for autosave is implemented twice: The asynchronous version is used for the interval trigger (every 2 minutes in my case, adjust to liking) and the synchronous version for the window unload event (asynchronous does not work for this event).
Also, note there is an element with class "auto-save-info". This can display the state of the last autosave operation (either the time of last autosave or a message that the autosave has failed). To account for that, add the following function to your ActiveForm class:
public function autoSaveInfo ($htmlOptions = array ())
{
if (isset ($htmlOptions['class']))
$htmlOptions['class'] += ' auto-save-info';
else
$htmlOptions['class'] = 'auto-save-info';
return Chtml::tag ('div', $htmlOptions, Yii::t('global','Last auto-save: {autoSaveInfo}', array('{autoSaveInfo}'=>CHtml::tag('span'))));
}
3. Extend CController
Same thing as above. Create Controller.php in your components folder and do a "class Controller extends CController". Your Controller class needs a function that receives the auto-save ajax calls and stores the data in your database (using AR in my case):
protected function handleAutoSave ($model)
{
$model->attributes = $_POST[$model->getClassName()];
$output = array ('success'=>false, 'time'=>'');
if ($model->validate ())
{
$model->save (false);
$output['time'] = Yii::app()->locale->getDateFormatter()->formatDateTime(time(), null,'medium').' '.Yii::t('global','o\'clock');
$output['success'] = true;
}
echo CJSON::encode ($output);
}
4. Extend CActiveRecord
You probably noticed the “getClassName()” function call. This leads us to extending CActiveRecord (same as with CActiveForm and CController). Add the following function to your ActiveRecord class which extends Yii’s CActiveRecord class in your components folder:
public function getClassName ()
{
return get_called_class ();
}
5. Putting everyting together
Now, we have everything we need. To implement an auto-save form, do the following:
First, in your view file, start your form like this:
<?php $form=$this->beginWidget('ActiveForm', array(
'id'=>'autosave-form',
'enableAutoSave'=> true,
)); ?>
Note that it is "ActiveForm", not "CActiveForm". Each call to "$form->textArea()" or "$form->textField" will now result in an auto-save-enabled field. Use "<?php echo $form->autoSaveInfo()?>" to display the auto-save information.
Second, augment your controller action with the following code in order to auto-save. It needs to stand after you load the model but before you would save a regular form submission.
// fetch your $model before-hand, e.g. $model = MyModel::model()->findByPk($_GET['id']);
// autosave
if(isset($_POST['autosave']) && $_POST['autosave']==='autosave-form')
{
$this->handleAutoSave ($model);
Yii::app()->end();
}
One last thing: in your css file, you should add the following:
.auto-save-info {
visibility: hidden
}
This hides the auto-save information until the first auto-save operation.
That’s it. I hope it’s helpful and not too full of bugs. I am sure that the code can be optimized and made a bit easier, but this worked quite well for me. If you have suggestions please let me know!