Git Product home page Git Product logo

yii2-sortable-behavior's Introduction

Yii2 Sortable

Latest Stable Version Total Downloads License

This package contains five classes to handle the sorting of ActiveRecords:

  • SortableGridView - extended GridView widget;
  • SortableListView - extended ListView widget;
  • Sortable - ActiveRecord Behavior to handle the sorting of the records themselves, or of the one-to-many related records;
  • PivotRecord - base class for the ActiveRecord of the pivot table in a many-to-many relation.
  • MMSortable - ActiveRecord Behavior to handle the sorting of many-to-many related records.

The previous version of Sortable was dependent on jQuery. It now has no dependencies at all. It should work in all modern desktop browsers. At the time of writing the new HTML Drag and Drop, and thus Sortable, only works for few mobile browsers.

The old jQuery-dependent widgets are still available. Some may prefer their esthetics.

A demonstration of the Sortable suit is here.

Installation

The preferred way to install Sortable is through Composer. Either add the following to the require section of your composer.json file:

"sjaakp/yii2-sortable-behavior": "*"

Or run:

composer require sjaakp/yii2-sortable-behavior "*"

You can manually install Sortable by downloading the source in ZIP-format.

SortableGridView and SortableListView

These widgets are derived from the standard GridView and ListView classes, but have one extra capability: the items can be moved to another position by means of drag and drop (using HTML Drag and Drop functionality). If an item is dropped on a new position, a message is posted with the following data:

  • key: the value of the item's primary key;
  • pos: the zero-indexed new position of the item.

SortableGridView and SortableListView have one configurable property:

orderUrl

array|string. The URL which is called after a sorting operation. The format is that of yii\helpers\Url::toRoute.

SortableGridView and SortableListView don't go together with the Pjax-widget. However, Sortable is not very usable with paged data-widgets anyway.

Sortable

With this Behavior, an ActiveRecord becomes 'sortable'. It has one configurable property and one extra method:

orderAttribute

string|array. The order attribute(s) of the ActiveRecord.

This can take the following values:

  • string - the order attribute name;
  • array of:
    • string - the order attribute name,
    • foreignKeyName => orderAttrName - limit ordering to ActiveRecords with the same foreign key value, i.e. of the same owner.

Default is "ord".

order()

public function order( $newPosition, $foreignKeyName = null, where = [] )

This method puts the owner on the new position $newPosition by manipulating the order attribute. The order attribute is a zero-indexed, contiguous integer.

If $foreignKeyName is null (default) all the records are ordered.

If it is a string, the ordering is restricted to the records with the same value of $foreignKeyName. $foreignKeyName must be a key in orderAttribute. This comes in handy with one-to-many relations.

$where is an array of extra conditions in the format used by
QueryInterface where(). May be used to add extra foreign key restraints. Default: [ ] (empty array).


Usage scenario 1

Simple sorting

Suppose we have a very simple table of movie titles:

CREATE TABLE movie (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  ord int(10) unsigned NOT NULL,
  title tinytext NOT NULL,
  PRIMARY KEY (id)
)

Where ord will be our order attribute.

We can make the Movie ActiveRecord sortable like this:

class Movie extends ActiveRecord
{
    public function behaviors( ) {
	    return [
	        [
	            'class' => 'sjaakp\sortable\Sortable',
	        ],
	    ];
	}
	...
}

In the controller we define an index action and an order action:

class MovieController extends Controller
{
	...
    public function actionIndex( )
	{
	    $dataProvider = new ActiveDataProvider( [
	        'query' => Movie::find( )->orderBy( 'ord' ),	// notice the orderBy clause
	        'sort' => false,
	        'pagination' => false
	    ] );

	    return $this->render( 'index', [
	        'dataProvider' => $dataProvider,
	    ] );
	}
	...
    public function actionOrder( )   {
        $post = Yii::$app->request->post( );
        if (isset( $post['key'], $post['pos'] ))   {
            $this->findModel( $post['key'] )->order( $post['pos'] );
        }
    }
	...
}

In the index view, we use a SortableGridView:

use sjaakp\sortable\SortableGridView;
...
<?= SortableGridView::widget( [
    'dataProvider' => $dataProvider,
    'orderUrl' => ['order'],
    'columns' => [
		...
        'title:ntext',
		...
    ],
	...
] ); ?>

And bingo! The list of movie titles is now sortable by drag and drop.


Usage scenario 2

One-to-many sorting

Suppose we also have a list of directors. Each director has many movies, each movie belongs to one director (thinking of the Coen brothers, I know this is not necessarily true in reality).

We add two columns to our movie table:

CREATE TABLE movie (
  id int(10) unsigned NOT NULL AUTO_INCREMENT,
  ord int(10) unsigned NOT NULL,
  title tinytext NOT NULL,
  director_id int(10) unsigned NOT NULL,
  director_ord int(10) unsigned NOT NULL,
  PRIMARY KEY (id)
)

Where director_ord is the order attribute just for movies belonging to the same director.

In the Director model we define a one-to-many relation, like we would normally do (notice the orderBy clause):

class Director extends ActiveRecord	{
	...
	public function getMovies( ) {
    	return $this->hasMany( Movie::class, ['director_id' => 'id'] )
    	    ->orderBy( 'director_ord' );
	}
	...
}

The Movie model is sortable like before, but with another orderAttribute:

class Movie extends ActiveRecord
{
    public function behaviors( ) {
	    return [
	        [
	            'class' => 'sjaakp\sortable\Sortable',
                'orderAttribute' => [
                    'director_id' => 'director_ord'
	            ]
	        ],
	    ];
	}
	...
}

This time, DirectorController sports an extra action:

class DirectorController extends Controller
{
	...
    public function actionMovieOrder( )   {
        $post = Yii::$app->request->post( );
        if (isset( $post['key'], $post['pos'] ))   {
            $movie = Movie::findOne( $post['key'] );
            if ($movie) $movie->order( $post['pos'], 'director_id' );
        }
    }
	...
}

Let's now use a SortableGridView to display all the movies of the director in director/view:

use sjaakp\sortable\SortableGridView;
...
$movies = new ActiveDataProvider( [
	'query' => $model->getMovies( ),  // Do not use $model->movies, it returns array of Movies in stead of an ActiveQueryInterface
	'sort' => false,
	'pagination' => false
] );
...
<h1><?= Html::encode( $model->name ) ?></h1>
...
<?= SortableGridView::widget( [
    'dataProvider' => $movies,
    'orderUrl' => ['movie-order'],
    'columns' => [
		...
        'title:ntext',
		...
    ],
	...
] ); ?>

Now each director's view shows a sortable list of his or her movies.

It's easy to combine Usage scenario's 1 and 2, so that all movies are sortable in movie/index and only the director's movies in director/view. Just initialize Movie's Sortable behavior like this:

class Movie extends ActiveRecord
{
    public function behaviors( ) {
	    return [
	        [
	            'class' => 'sjaakp\sortable\Sortable',
                'orderAttribute' => [
					'ord',
                    'director_id' => 'director_ord'
	            ]
	        ],
	    ];
	}
	...
}

PivotRecord

This is the base ActiveRecord for the pivot table of two sortable Models in a many-to-many relation.

The ordering information is stored in the pivot table as well.

A pivot table might look something like this:

CREATE TABLE actor_movie (
  actor_id int(10) unsigned NOT NULL,	# actor's primary key
  movie_id int(10) unsigned NOT NULL,	# movie's primary key
  actor_ord int(10) unsigned NOT NULL,	# actor's order
  movie_ord int(10) unsigned NOT NULL,	# movie's order
  PRIMARY KEY (actor_id,movie_id),
)

Using best practices it means:

  • the table name is a concatenation of the two related table names in lexicographic order, separated by an underscore ('_');
  • the primary key column names consist of the related table name followed by '_id';
  • the order column names consist of the related table name followed by '_ord';

Of course, it would be wise to add some indexes.

A concrete pivot record has to be derived from PivotRecord. Two static functions must be defined in the derived class:

aClass() and bClass()

protected static function aClass( ) protected static function bClass( )

These static member functions should return the fully qualified class names of the related Models.

aClass and bClass are completely equivalent. PivotRecord is in any respect a symmetric class.

A complete definition of a pivot record might look like this:

namespace app\models;
use sjaakp\sortable\PivotRecord;

class MovieActor extends PivotRecord    {

    protected static function aClass( )   {
        return Movie::class;
    }

    protected static function bClass( )   {
        return Actor::class;
    }
}

Notice that you can define some other static values as well, for special cases. Refer to the source code if you need this.

A PivotRecord-derived class has the following extra functions.

getAs() and getBs()

public static function getAs( ActiveRecord $b )

Get the ordered records of classA belonging to classB $b.

public static function getBs( ActiveRecord $a )

Get the ordered records of classB belonging to classA $a.

The result is returned as an ActiveQuery, which can be modified further, or used as source of an ActiveDataProvider.

Notice these are static functions, not referring to any instantiation of the PivotRecord-derived class.

orderA() and orderB()

public function orderA( $newPosition )

Place classA at $newPosition in the list of all classA's belonging to classB.

public function orderB( $newPosition )

Place classB at $newPosition in the list of all classB's belonging to classA.

These are member functions. The id's of classA and classB are stored in the current PivotRecord.

MMSortable

This is a Behavior of both partner ActiveRecords in a many-to-many relations. PivotRecord relies on it. MMSortable performs some housekeeping and has no (interesting) member functions. However, two properties have to be configured:

pivotClass

string. The fully classified class name of the pivot class (the PivotRecord-derived class).

pivotIdAttr

string. The attribute name of the owner's id in the pivot class. If this is not set, it will be derived from the owner's class name; for instance: if the owner is class Movie, $pivotIdAttr will be "movie_id".


Usage scenario 3

Many-to-many sorting

Apart from our movie table, we also have an actor table. They are linked via an actor_movie pivot table: each movie can have many actors, and each actor can have many movies.

First, we define a pivot class, like so:

namespace app\models;
use sjaakp\sortable\PivotRecord;

class MovieActor extends PivotRecord    {

    protected static function aClass( )   {
        return Movie::class;
    }

    protected static function bClass( )   {
        return Actor::class;
    }
}

Then, we make sure that both Movie and Actor have a MMSortable Behavior:

class Movie extends ActiveRecord	{
	...
    public function behaviors( ) {
        return [
            [
                'class' => 'sjaakp\sortable\MMSortable',
                'pivotClass' => MovieActor::class
            ]
        ];
    }
	...
}

For convenience, we add a very simple member function to Movie:

class Movie extends ActiveRecord	{
	...
    public function getActors( ) {
        return MovieActor::getBs( $this );
    }
	...
}

Define an order-actor-action in MovieController:

class MovieController extends Controller
{
	...
    public function actionOrderActor( $id )   {
        $post = Yii::$app->request->post( );
        if (isset( $post['key'], $post['pos'] ))   {
            $piv = MovieActor::find( )->where( [
                'movie_id' => $id,
                'actor_id' => $post['key']
            ] )->one( );
            $piv->orderB( $post['pos'] );
        }
    }
	...
}

Now, in movie/view, we can display a SortableGridView with all the actors appearing in the movie.

use sjaakp\sortable\SortableGridView;
...
$actors = new ActiveDataProvider( [
	'query' => $model->getActors( ),
	'sort' => false,
	'pagination' => false
] );
...
<h1><?= Html::encode( $model->title ) ?></h1>
...
<?= SortableGridView::widget( [
    'dataProvider' => $actors,
    'orderUrl' => ['order-actor', 'id' => $model->getPrimaryKey()],
    'columns' => [
		...
        'name:ntext',
		...
    ],
	...
] ); ?>

Sortable with jQuery

The previous version of Sortable (1.0) used jQuery Draggable and Sortable. The old jQuery-widgets are still available as SortableGridViewJquery and SortableListViewJquery. They are exchangeable with their non-jQuery counterparts.

You may prefer the esthetics of the jQuery variants. Also, the new HTML Drag and Drop may not work for all mobile browsers.

SortableGridViewJquery and SortableListViewJquery have two extra configurable properties:

sortOptions

array. The options for the jQuery sortable object. See https://api.jqueryui.com/sortable/.

Notice that the options 'items', 'helper', and 'update' will be overwritten.

Default: [] (empty array).

sortAxis

boolean|string The 'axis' option for the jQuery sortable. If false, it is not set. Default: 'y'.

For compatibility, SortableGridView and SortableListView have these options as well, but they are not functional.


Thanks

  • mike-kramer (sortAxis option)
  • menshakov (use updateAttributes)
  • robsch (subtle order bug)
  • rubenheymans (multiple foreign keys idea)

yii2-sortable-behavior's People

Contributors

menshakov avatar sjaakp avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

yii2-sortable-behavior's Issues

Multiple ordering

Hi!
I use your widget and it works fine.
But if I put tabels in rows, which I want sort, rows in that tabels becames drugable. It happen because
$view->registerJs("jQuery('#{$id} tbody').sortable($sortJson);");
It find all tbody in widget. Could you fix it by repalce this line by
$view->registerJs("jQuery('#{$id} > table > tbody').sortable($sortJson);");
or similar?

Multiple foreign keys

I have a Team module with a location_id and department_id as foreign keys

Order action:

public function actionOrder()
    {
        $post = Yii::$app->request->post();

        if (isset($post['key'], $post['pos'])) {
            $this->findModel($post['key'])->order($post['pos'], ['location_id', 'department_id']);
        }
    }

Changed the code in the Order function to support multiple foreign keys

foreach ($foreignKeyNames as $foreignKeyName) {

    // restrict order to records with the same foreign key value
    if (! is_array($this->orderAttribute) || ! isset($this->orderAttribute[$foreignKeyName]))
        throw new InvalidConfigException(get_called_class() . "::orderAttribute[$foreignKeyName] is not set.");

    $orderAttr = $this->orderAttribute[$foreignKeyName];
    $where[$foreignKeyName] = $owner->getAttribute($foreignKeyName);
}

Team model behavior:

'sortable' => [
    'class' => Sortable::className(),
    'orderAttribute' => ['position', 'location_id' => 'position', 'department_id' => 'position'],
],

Can you add this to the behavior?

editing a record reorders it to last in list.

It is bit weird... in localhost it works fine but in UNIX server, when i edit a record, the "ord" value is also updated to be the last in the list.

I have one to many relation

//movie model -- director id is foreign key public function behaviors() { return [ [ 'class' => 'sjaakp\sortable\Sortable', 'orderAttribute' => [ 'director_id' => 'ord' ] ], ]; }

and order function

public function actionOrder() { $post = Yii::$app->request->post(); if (isset($post['key'], $post['pos'])) { $model = $this->findModel($post['key']); if ($model) $model->order($post['pos'], 'director_id'); } }

Error when deleting a model

My column for the sorting is set to be unique in MySQL. This should be fine I think. However, when I delete an object beforeDelete will try to decrease the order number before deleting the actual record. But this cannot work since this would mean that the order number exists twice (for a short time). That is not allowed with a unique column.

Not so hard to create a workaround. But it would be better if the sortable behavior could deal with this. Would it be already solved if updating the order number after deleting the actual record?

Problem with order()

Hello. First thank you for this script. It will be very useful for me.
But for this moment I have problem. I copy everything like in instruction. And sorting works, but with small problem when I need to sort not all records in table.
For example, I have records in one table:
id=1, relative_id=1, position=0
id=2, relative_id=1, position=1
id=3, relative_id=1, position=2
id=4, relative_id=2, position=0

And when I make sorting with selected items by relative_id=1, for example I want id=3 to be first in the list. I drag item to the top. All related records update. But also updates unrelated records. In the end I have:
id=1, relative_id=1, position=1
id=2, relative_id=1, position=2
id=3, relative_id=1, position=0
id=4, relative_id=2, position=1
But id=4 wasn't even selected from database to model.

I think, method order() works incorrect. But I can't find information about this method.
$this->findModel( $post['key'] )->order( $post['pos'] ); What order() does?

Thank you!

Order error

Hi @sjaakp , thank you for great plugin, but i have one issue.

When i am sorting rows and saving with your action current position index it's working great. But if i am dragging for example five rows to same top i have five rows with order position = 0. They are not relative to each other so they are not sorted like on the list.

I think that it should returning data about dragged row and also rearranged rows caused by last action.

Maybe i've just configured something wrong.

Thank you for your help.

Cheers.

Not working after pjax refresh

When I use the filters, and the gridview refreshes (pjax), sorting is not working anymore
When I refresh the page, it's working again

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.