Easy autosave functionality for CActiveForm

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.

  1. 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.)

  1. Extend CActiveForm

  2. Extend CController

  3. Extend CActiveRecord

  4. 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!

Outstanding!

one thing: I guess it would make sense to extend the jQuery code such that it can handle several forms on one page.

I had implemented auto-save using perform Ajax validation function in controller.I saved model after Ajax validation the only difference was that I did not checked for required validations.It worked for me and form was saved for every onBlur event. ::)

@Mangesh Lad; Would you care to enlighten us with a little bit more info/code about where and what you did for ‘auto-save using perform Ajax validation function in controller’? Very much appreciated… :)

Hi befi,

Thanks for sharing your code snipped, very useful :)

However I’m a bit new to Yii and I’d like to apply autosave to some forms of my application, but not ALL of them.

How should I process? Which step should I perform?

Thanks a lot!

hi, i’m new in yii FrameWork, and i’m having problems with the inheritance of the method getClassName that estends from CActiveRecord, my ASActiveForm Extends From CActiveForm, My Controller Extends from HTController which Extends from CController, but when it makes the ajax to perform the autosave it says:

‘MyController’ and its behaviors doesn’t have a function or closure called “getClassName”. (C:\xampp\htdocs\yii\framework\base\CComponent.php:265)</p><pre>#0 C:\xampp\htdocs\yii\framework\db\ar\CActiveRecord.php(225): CComponent->__call(‘getClassName’, Array)

thanks for the answer