Yii 1.1: Creating a jQueryUI Sortable CGridView

54 followers

I have had to do this a couple of times now so I figured I would share it with the community. I am going to keep this short because I really hope that you are familiar with jQueryUI's Sortable class before starting this tutorial.

Here are the basic steps to achieve this:

  1. Make sure your database table has a 'sortOrder' field.
  2. (Optional) Add the 'sortOrder' field to your Rules() function in your model
  3. Add the 'actionSort()' method to your controller to apply the sorting via ajax
  4. Add jQuery UI to your view that has the CGridView
  5. Setup the jQuery UI Code to work with the CGridView in question
  6. (Optional) Apply the sorting to your model's search() function if need be

So lets get started!

Step 1 is self-explanatory, just make sure that you have an INT field in your database to store the sortOrder of each item ( for this article we will call this field 'sortOrder' )

Step 2: This step is optional but recommended. Add this line to the model who's items you are sorting.

array('sortOrder', 'numerical', 'integerOnly'=>true),

Step 3: This is the part where we add the code to a controller that will apply the new sorting order to the rows in your database. Note I will be using a controller titled 'Project'. You will see me link to this controller in the jQueryUI Sortable javascript code.

  • Note: Please make sure that the user who will be doing the sorting has access to this action in your 'accessRules()' for the controller in question.

'Project' is the model that I am applying the ordering to. You can add a CDbCriteria in here if you need to sort just specific rows.

public function actionSort()
{
    if (isset($_POST['items']) && is_array($_POST['items'])) {
        $i = 0;
        foreach ($_POST['items'] as $item) {
            $project = Project::model()->findByPk($item);
            $project->sortOrder = $i;
            $project->save();
            $i++;
        }
    }
}

Step 4: OK Here is the fun part. We need to setup jQuery UI to link to the CGridView w/o having to modify the source of the CGridView in any way. By doing this we don't have to modify any of the core files or extend CGridView in any way.

<?php
    $str_js = "
        var fixHelper = function(e, ui) {
            ui.children().each(function() {
                $(this).width($(this).width());
            });
            return ui;
        };
 
        $('#project-grid table.items tbody').sortable({
            forcePlaceholderSize: true,
            forceHelperSize: true,
            items: 'tr',
            update : function () {
                serial = $('#project-grid table.items tbody').sortable('serialize', {key: 'items[]', attribute: 'class'});
                $.ajax({
                    'url': '" . $this->createUrl('//project/sort') . "',
                    'type': 'post',
                    'data': serial,
                    'success': function(data){
                    },
                    'error': function(request, status, error){
                        alert('We are unable to set the sort order at this time.  Please try again in a few minutes.');
                    }
                });
            },
            helper: fixHelper
        }).disableSelection();
    ";
 
    Yii::app()->clientScript->registerScript('sortable-project', $str_js);
?>
<?php $this->widget('zii.widgets.grid.CGridView', array(
    'id'=>'project-grid',
    'dataProvider'=>$model->search(),
    'filter'=>$model,
    'rowCssClassExpression'=>'"items[]_{$data->id}"',
    'columns'=>array(
        'id',
        'title',
        'categoryId',
        'sortOrder',
        array(
            'class'=>'CButtonColumn',
        ),
    ),
)); ?>

OK So first off we are setting up the jQueryUI Sortable object in javascript and attaching it to our CGridView. Things to note are: 'project-grid' is the 'id' of our CGridView and will need to be switched to the ID of your CGridView. Also, the following line will need to be altered to point to your //controller/action path where you added the 'actionSort()' function:

'url': '" . $this->createUrl('//project/sort') . "',

Once the javascript is setup we need to setup the CGridView. The only two things out of the ordinary are that we are explicitly setting the 'id' of the CGridView and we are also setting the 'rowCssClassExpression' variable to work with jQueryUI Sortable.

'id'=>'project-grid',
'rowCssClassExpression'=>'"items[]_{$data->id}"',

Step 5: In your view file or controller that is displaying the CGridView you will need to tell Yii to add jQueryUI to your page.

Yii::app()->clientScript->registerScriptFile('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js');

Step 6: If you are planning on using your model::Search() function to setup DataProviders for things like CListView and CGridView I recommend you add the following line to the Search() function towards the bottom before the CActiveDataProvider is created:

$criteria->order = 'sortOrder ASC';

I hope this helps some people out! If you are having any problems please go through this write-up again to make sure you didn't miss anything.

Total 20 comments

#16750 report it
rawlly at 2014/03/25 05:52am
Serialize another variable in the POST to server?

How do you serialize another item and send back in the POST? In parallel with ITEMS?

#16679 report it
YiiJeka at 2014/03/18 07:53am
Fixes absolute position on big grid

Just add style "overflow: auto" for '#project-grid',

#16095 report it
dignityinside at 2014/01/19 05:21pm
Thank you

Thank you very much. Work fine.

I'm only added one line (for load jquery ui):

Yii::app()->clientScript->registerCoreScript('jquery.ui');
#15315 report it
Chris Backhouse at 2013/10/28 04:23am
ActionSort

Alternative and simpler sort

public function actionSort() {
            if (isset($_POST['items']) && is_array($_POST['items'])) {
                // Get all current target items to retrieve available sortOrders
                $cur_items = Photos::model()->findAllByPk($_POST['items'], array('order'=>'sort_order'));
        foreach ($cur_items as $cur_item) {
            $pl=array_search($cur_item->id,$_POST['items']);
            if ($cur_item->sort_order!=$pl) {
                $cur_item->sort_order=$pl;
                $cur_item->save();
           }
        }
            }
        }
#15289 report it
Daniel Galvan at 2013/10/23 09:23pm
I have to say it is a great Wiki.

Thank you for sharing this.

#14680 report it
skysham at 2013/09/03 05:59am
Great Wiki!

I have add this code to update the CGridView after sorted. Thanks!

$('#project-grid table.items tbody').sortable({
    ....
    'success': function(data){
        $.fn.yiiGridView.update('project-grid');
    },
    ....
}).disableSelection();
#14380 report it
Aneesh Asokan at 2013/08/07 09:36am
Not working after adding a file field in model

Hi, i tried this in grid view. But after adding a file field in model, there is no change happening in database. The drag effects works fine. But the order nit saved in the database. How it can solve? Model filed - array('image_file', 'file','types'=>'jpg, gif, png'),

Thanks.

#13755 report it
skeef at 2013/06/23 12:54am
About Sorting

Each time the record was deleted the natural sort order is also broke - there are a some spaces in the order numbers. To beautiful and revolving series of numbers always been to sort (1,2,3,4,5 ...) I offer a solution:

public function actionSort()
{
    if( Yii::app()->request->isAjaxRequest )
    {
        if( isset( $_POST[ 'items' ] ) && is_array( $_POST[ 'items' ] ) )
        {
            foreach( $_POST[ 'items' ] as $key => $val )
            {
                MODEL::model()->updateByPk( $val, array (
                    'sortOrder' => ( $key + 1 )
                ) );
            }
        }
    }
}

I noticed that the lines always come in the order that the user has created. Therefore, it remains just record this order in the Database. It should be noted that such code heavy load database as each row in the range of sorting always overwritten again. Therefore, it should be used with caution in lightly loaded tables. For example, in the Admin Panel.

#13592 report it
tomvdp at 2013/06/09 04:34pm
Beware when using the selectionChanged event

If you are using the selectionChanged event on the grid, then you probably have some code to get the id of the selected row and then do something with that. You normally get the id using getSelection:

 function myGridChange(id) {
     doSomething($.fn.yiiGridView.getSelection(id));
 }

After the rows are sorted using the mechanism here described, getSelection will return the wrong id. This is because the grid stores its keys outside the table of the grid, in a hidden div. Those keys are not re-ordered when dragging and dropping a row.

Edit: Here is a solution: use jQuery to find the selected row. It searches for the row that has the value 'selected' added to its class. I am not sure that this will always be set before the selectionChanged event is fired. It obviously will not work if your grid allows multiple rows to be selected at the same time, i.e. selectableRows>1.

function myGridChange(id) {
     doSomething($('#myGrid tr.selected').attr('class').split('_')[1]);
}
#12990 report it
kicoe at 2013/04/25 09:08am
Update jquery-ui

Hi,

I was having a javascript error : "a.curCSS is not a function".

Found why : The version of jquery ui used in this wiki is outdated regarding the jquery version provided in fresh yii install. I recomand using ajax.googleapis.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js, worked for me.

Great wiki though, worked perfectly for me thank you!

#10902 report it
LeiLi at 2012/12/01 08:43pm
Awesome post

It works like charm. FYI, for guys like me who know little about jQuery UI, there is nothing to download or install. Just follow the this post and pay special attention to the filter part in the controller if you have one. For me, I was getting the Not Found error because the filter I set up in my controller applied to all actions which including sort action. Nice post!!

#8365 report it
mnogokotin at 2012/05/29 04:44am
thank you

it works. thanks

#6585 report it
softark at 2012/01/19 11:58am
CGridView::afterAjaxUpdate

Thank you blindmoe for this great article. It helped me a lot.

And I'd like to share my experience with you all. I think I have come up with a solution for the ajax update problem.

You can make use of the CGridView::afterAjaxUpdate to make the grid sortable again after it is updated by ajax. (The situation can occur when you have applied a filter, deleted an item, moved to another page by pagination, or changed the item count in the page.)

First, enclose the installation code of sortable into a function, and call it from $(document).ready() to make the grid sortable for the initial loading of the page.

Instead of:

$('#some-grid table.items tbody').sortable({
    ...
}).disableSelection();

Write like this:

function installSortable() {
    $('#some-grid table.items tbody').sortable({
        ...
    }).disableSelection();
}
installSortable();

And call the installation function after the grid has been updated by ajax ... this will be done via CGridView::afterAjaxUpdate.

function reInstallSortable(id, data) {
    installSortable();
}
...
 
$this->widget('zii.widgets.grid.CGridView', array(
    'id' => 'some-grid',
    ...
    'afterAjaxUpdate' => 'reInstallSortable',
...

And, at last, you have to modify the sort action in the controller to give the items the correct sort order numbers when they are filtered or paginated. You should not simply give them a series of number from 0 to n-1. They can collide with the sort order numbers that other items not in the current page might have.

First, retrieve all the sort order numbers that the items in the current page have, then re-assign them to the listed items.

public function actionSort() {
    if (isset($_POST['items']) && is_array($_POST['items'])) {
        // Get all current target items to retrieve available sortOrders
        $cur_items = Item::model()->findAllByPk($_POST['items'], array('order'=>'sortOrder'));
        // Check 1 by 1 and update if neccessary
        for ($i = 0; $i < count($_POST['items']); $i++) {
            $item = Item::model()->findByPk($_POST['items'][$i]);
            if ($item->sortOrder != $cur_items[$i]->sortOrder) {
                $item->sortOrder = $cur_items[$i]->sortOrder ;
                $item->save();
            }
        }
    }
}
#6584 report it
softark at 2012/01/19 10:56am
CSRF

The CSRF token and it's name can be retrieved from CHttpRequest. There's no need to wrap the grid in a form. You can write like this ...

$csrf_token_name = Yii::app()->request->csrfTokenName;
$csrf_token = Yii::app()->request->csrfToken;
...
$str_js = "
...
serial = $('#some-grid table.items tbody').sortable('serialize', {key: 'items[]', attribute: 'class'}) + '&{$csrf_token_name}={$csrf_token}';
...
";
#5873 report it
mikewalen at 2011/11/21 04:56pm
Excellent!

Great wiki blindmoe. Just wondered if anyone has come up with a solution yet to keep the sortable function working after changing the page or filtering (as mentioned by SSaarik in the previous post)? My jQuery knowledge is minimal and I just don't know where to begin.

In the meantime, I've read that disabling ajax updates on the grid can help. Add the following line to the CGridView configuration array in your View...

'ajaxUpdate'=>false,
#5647 report it
SSaarik at 2011/10/28 09:38am
Ajax

How can I make this work with filters enabled? After filtering sortable function drops.

Trying to figure it out, I'll give solution if I find one.

#5378 report it
Roman Solomatin at 2011/10/08 10:50am
Just what I have been looking for

Thanks! I'll try this out right away.

#5317 report it
Wiseon3 at 2011/10/04 03:24am
Fix CSRF validation

@skeef This is a common problem when you enable CSRF validation and use AJAX in your pages. To fix it in this case wrap the grid in form tag (to be sure the CSRF token is present at least once in the page):

echo CHtml::form();
$this->widget('zii.widgets.grid.CGridView', array(
....
));
echo CHtml::endForm();

In your javascript file where you do the ajax call you need to send the CSRF token as well:

'YII_CSRF_TOKEN':document.getElementsByName('YII_CSRF_TOKEN')[0].value
or
'YII_CSRF_TOKEN':$('input[name="YII_CSRF_TOKEN"]').val()

You could try changing the code written above to:

update : function () {
                serial = $('#project-grid table.items tbody').sortable('serialize', {key: 'items[]', attribute: 'class'}) + '&YII_CSRF_TOKEN=' + document.getElementsByName('YII_CSRF_TOKEN')[0].value;
#5306 report it
skeef at 2011/10/03 07:24am
CSRF token Error

Thank you for yuor work. All told accessible and understandable.

But I have some Error.

If

'request'=>array(
    'enableCsrfValidation'=>true,
),

in FirePHP I see an Error:

The CSRF token could not be verified

Prompt solution, please

#5156 report it
blindMoe at 2011/09/19 04:48pm
Answer about #2

Allain, I think you are actually correct, but it is a good practice to have validation rules for all of the properties in your models. I will switch the verbiage to specify that #2 is optional.

Leave a comment

Please to leave your comment.

Write new article