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:
- Make sure your database table has a 'sortOrder' field.
- (Optional) Add the 'sortOrder' field to your Rules() function in your model
- Add the 'actionSort()' method to your controller to apply the sorting via ajax
- Add jQuery UI to your view that has the CGridView
- Setup the jQuery UI Code to work with the CGridView in question
- (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: ~~~ [javascript] '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. ~~~ [javascript] '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.
Simply Beautiful
Thank you for this, saved me a lot of blind guessing.
Question about #2.
Can you explain this statement, since sortOrder isn't being provided by the user and isn't being loaded using $project->attributes, I'm not sure this is necessary.
I've incorporated this into my project and confirmed that all works fine without it.
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.
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
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;
Just what I have been looking for
Thanks! I'll try this out right away.
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.
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,
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}'; ... ";
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(); } } } }
thank you
it works. thanks
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!!
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!
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]); }
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.
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.
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();
I have to say it is a great Wiki.
Thank you for sharing this.
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(); } } } }
Thank you
Thank you very much. Work fine.
I'm only added one line (for load jquery ui):
Yii::app()->clientScript->registerCoreScript('jquery.ui');
Fixes absolute position on big grid
Just add style
`
"overflow: auto"for
'#project-grid',`
Serialize another variable in the POST to server?
How do you serialize another item and send back in the POST? In parallel with ITEMS?
Yii 1.1.16 (with jQuery > 1.9)
I don't know why, but with last Yii version 1.1.16 this approach won't work anymore in my applications.
jQuery method "serialize" won't take care of class="items[]_n" attribute because it seems to parse also its automatically added "ui-sortable-handle".
So I needed to change some lines of this good tutorial:
[javascript] $('#myTable table.items tbody').sortable({ ... update : function () { items = $(this).sortable('toArray', {attribute: 'class'}); // switch from serialize to toArray $.ajax({ url: '{$url}', type: 'post', data: {items: items}, // sending the array to controller ... }); }, ... }).disableSelection();
YourController.php
public function actionSort() { if (isset($_POST['items']) && is_array($_POST['items'])) { $i = 0; foreach ($_POST['items'] as $item) { $number = preg_replace("/[^0-9]/", "", $item); if(($model = Project::model()->findByPk((int)$number)) !== null) { $model->order = $i; $model->save(); $i++; } } } }
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.