Ordenar items con KnockOut Sortable

Normalmente, todo lo referente a draggable, dropable y/o sortable no es tan bonito como lo suelen poner en los ejemplos. A la mínima que empezamos a adaptarlo a nuestras necesidades, acaban surgiendo algunos problemas y nos tenemos que pasar la primera vez perdiendo bastante tiempo. Si tenemos que utilizar KnockOut nos encontramos KnockOut-Sortable, nos permitirá ordenar listados de ítems de forma bastante fácil.

KnockOut-Sortable, como he comentado, se encarga de ordenar mediante dragg & drop un listado, mover entre diferentes listados e incluso simular un dragg & drop del estilo visual studio en winforms (cuando podíamos arrastrar controles a un formulario n veces). Funciona bien, que es lo importante, y te mantiene el ítem de knockout

Aquí veremos 3 ejemplos, uno que será el típico y simple, una ordenación de una lista. El otro será enfocado a las listas anidadas, en más de una ocasión he tenido que mover ítems entre listas y estos ítems conteniendo otros donde se podía hacer el dragg & drop entre ellos. Finalmente, os mostraré un ejemplo de listas conectadas.

Primer ejemplo: Lista rápida

Como he comentado, vamos a hacer una lista de ítems que sean ordenables. Lo primero que tenemos que hacer es tener KnockOut en nuestro proyecto y que se cargue, lo podéis sacar de algún gestor de paquetes o directamente de la página oficial de KnockOutJs. Después tenemos que bajarnos Knockout-sortable de aquí.

Ahora que tenemos todo, creamos el ViewModel que quedará tal que:

    <script type="text/javascript">
        (function () {
            //#region ViewModel
            var self = this;

            self.items= ko.observableArray([
                  { id: 1, descripcion: 'Item 1' },
                  { id: 2, descripcion: 'Item 1' },
                  { id: 3, descripcion: 'Item 1' },
                  { id: 4, descripcion: 'Item 1' },
                  { id: 5, descripcion: 'Item 1' },
            ]);
            //#endregion ViewModel

            ko.applyBindings(self);
        })();
    </script>

Esto lo pondremos en el mismo fichero que el html, pero se debería dividir en diferentes. Lo único que hemos hecho es crear un array como observableArray, y aplicar los bindings. Después, el lo que necesitamos en la vista sería de este estilo:

    <div>
        <ul data-bind="sortable: items">
            <li>
                 <span data-bind="text: descripcion"/>
            </li>
        </ul>
   </div>

He utilizado ul/li pero podéis utilizar cualquier otro ítem. Con el tag de sortable, indicamos que el contenedor es el ítem con el que podemos hacer «dragg». Este tag también hace un «foreach» sobre el array de ítems.

Este es un ejemplo rápido de cómo lo utilizaríamos con una lista sencilla, pero vamos a ir añadiendo cosas :).

Segundo ejemplo: Listas anidadas

Vamos a modificar el ejemplo anterior para ver como anidar ítems, como veremos es bastante fácil, pero repito, iremos añadiendo cositas.

Modificamos el ViewModel para que contenga una lista de subitems.

    <script type="text/javascript">
        (function () {
            //#region ViewModel
            var self = this;

            self.items= ko.observableArray([
                  { 
                     id: 1, 
                     descripcion: 'Item 1',
                     subItems: ko.observableArray([
                                   {
                                       id: 1, 
                                       descripcion: 'Sub - Item 1'
                                   },{
                                       id: 2, 
                                       descripcion: 'Sub - Item 2'
                                   },{
                                       id: 3, 
                                       descripcion: 'Sub - Item 3'
                                   },{
                                       id: 4, 
                                       descripcion: 'Sub - Item 4'
                                   }
                               ])
                  },{ 
                     id: 2, 
                     descripcion: 'Item 2',
                     subItems: ko.observableArray([
                                   {
                                       id: 5, 
                                       descripcion: 'Sub - Item 1'
                                   },{
                                       id: 6, 
                                       descripcion: 'Sub - Item 2'
                                   },{
                                       id: 7, 
                                       descripcion: 'Sub - Item 3'
                                   },{
                                       id: 8, 
                                       descripcion: 'Sub - Item 4'
                                   }
                               ])
                  },
                  { 
                     id: 3, 
                     descripcion: 'Item 3',
                     subItems: ko.observableArray([
                                   {
                                       id: 9, 
                                       descripcion: 'Sub - Item 1'
                                   },{
                                       id: 10, 
                                       descripcion: 'Sub - Item 2'
                                   },{
                                       id: 11, 
                                       descripcion: 'Sub - Item 3'
                                   },{
                                       id: 12, 
                                       descripcion: 'Sub - Item 4'
                                   }
                               ])
                  },
                  { 
                     id: 4, 
                     descripcion: 'Item 4',
                     subItems: ko.observableArray([])
                  },
                  { 
                     id: 5, 
                     descripcion: 'Item 5',
                     subItems: ko.observableArray([])
                  },
            ]);
            //#endregion ViewModel

            ko.applyBindings(self);
        })();
    </script>

Y la vista la modificaríamos para que quedase:

    <div>
        <ul data-bind="sortable: { data: items}" >
            <li>
                <span data-bind="text: descripcion"></span>
                      
                 <ul data-bind="sortable: { data: subItems}" >
                     <li>
                         -<span data-bind="text: descripcion"></span>
                     </li>
                 </ul>
            </li>
        </ul>
   </div>

Con esto podemos ver como pintan las listas anidadas y como interactúa con los otros subItems, pero vemos que los subItems se pueden poner como ítems y viceversa. Aparte, tenemos un bug porque los subItems no tienen la propiedad subItems cuando los arrastramos a nivel de ítems.

Una solución para esto serían las listas conectadas.

Tercer ejemplo: Listas conectadas

KnockOut Sortable tiene la propiedad connectClass y sirve para cuando queremos mover ítems entre diferentes listas pero que no interactúe con otras. En nuestro caso vamos a hacer que las diferentes listas de subItems interactuen entre ellas, pero no se pueda meter un subItem como ítem. Básicamente, esta propiedad añade el parámetro como clase y las conecta entre si, el ejemplo de antes lo podríamos arreglar modificando el código html por:

    <div>
        <ul data-bind="sortable: { data: items, connectClass: 'items'}" >
            <li>
                <span data-bind="text: descripcion"></span>
                      
                 <ul data-bind="sortable: { data: subItems, connectClass: 'subItems'}" >
                     <li>
                         <span data-bind="text: descripcion"></span>
                     </li>
                 </ul>
            </li>
        </ul>
   </div>

Si inspeccionamos el código resultado, veremos que en las «uls» nos ha puesto la clase pertinente y no nos permite poner ítems como subItems, ni subItems como ítems.

Aparte, podemos añadir templates para tener nuestro código más limpio, tenemos los eventos de antes y después de mover un ítem,… Todo lo podéis sacar directamente del repositorio, hay varios ejemplos que podéis seguir para ver su funcionamiento aquí