Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding

Este post forma parte de una serie de cuatro:

Configurando el Data Binding

Después de crear las vistas y definir el ViewModel, llega el momento de enlazar ambas partes e ir terminando con el tutorial. Cuando se aplica el patrón MVVM, lo más habitual (me atrevería a decir que casi obligatorio para que tenga sentido el patrón) es emplear algún tipo de data binding declarativo para enganchar la vista y el ViewModel.

Esa es precisamente la especialidad de Knockout. Mediante el uso del atributo data-bind aplicado a los elementos html podremos cumplir con tres requisitos fundamentales:

  • Mostrar en pantalla datos del ViewModel.
  • Actualizar el ViewModel cuando se modifiquen datos en la pantalla.
  • Invocar acciones sobre el ViewModel en respuesta a acciones del usuario.

Vamos a ver cómo podemos enlazar nuestro ViewModel con la estructura de vistas que teníamos.

La lista de armas

El código de la pantalla con la lista de armas queda así:

<div data-role="page" id="home">
    <div data-role="header">
        <h1>W.I.Z.A.R&D</h1>
       <a href="#" data-role="button" data-icon="add" class="add-new-note ui-btn-right" data-bind="jqmChangePage: '#addNew'">Añadir</a>
    </div>

    <div data-role="content">
        <ul data-role="listview" data-bind="foreach: weapons">
            <li>
               <a href="#" data-bind="{ text: name, click: $parent.edit, jqmChangePage: '#edit' }"></a>
               <a href="#" data-bind="{ click: $parent.remove }" data-role="button" data-icon="delete" data-theme="d"></a>
            </li>
        </ul>
       
    </div>
    <div data-role="footer" data-position="fixed">
       <h3>Peso Total: <span data-bind="text: totalWeight"></span> kgs.</h3>
    </div>
</div>

Esto nos permite ver unas cuantas cosas interesantes:

Se pueden enlazar propiedades al contenido de elementos html, como el caso del peso total que enlazamos con:

<span data-bind="text: weight">

Si queremos enlazar un array de objetos, como el caso de la lista de armas, debemos usar:

<ul data-bind="foreach: weapons">

Knockout es inteligente y tomará el contenido del elemento html enlazado con el foreach como una plantilla a aplicar a cada uno de los valores contenidos en el array. En este caso la plantilla aplicada a cada arma de la lista queda definida como:

<li>
    <a href="#" data-bind="{ text: name, click: $parent.edit, jqmChangePage: '#edit' }"></a>
    <a href="#" data-bind="{ click: $parent.remove }" data-role="button" data-icon="delete" data-theme="d"></a>
</li>

En esta plantilla vemos un ejemplo de como se pueden desencadenar acciones sobre el modelo:

<a href="#" data-bind="{ click: $parent.remove }" … />

En este caso además se emplea otra característica interesante de Knockout: el uso de $parent para referenciar el ViewModel padre de lo que estamos enlazando. Cuando aplicamos data binding a una lista o, en general, a un ViewModel que forma parte de otro, puede ser necesario invocar una acción del modelo padre. Para ello se debe prefijar el método a invocar con $parent.

Por último, también es importante destacar el uso de un custom binding para realizar la navegación entre páginas. Con el fin de desacoplar por completo el ViewModel de la vista (y de jQuery Mobile), estamos usando un custom binding que se encarga de realizar la navegación, como en el caso de la edición de un arma:

<a href="#" data-bind="{ text: name, click: $parent.edit, jqmChangePage: '#edit' }">

Con ese código estamos indicando varias cosas:

  • Que el texto del elemento <a> será el nombre del arma.
  • Que cuando se dispare el evento click, se debe invocar la función edit del padre del elemento enlazado (en este caso, MainViewModel).
  • Que al pulsar sobre el enlace queremos cambiar a la página de jQuery Mobile con id #edit.

Para implementar un custom binding sólo hace falta añadirlo al objeto ko.bindingHandlers:

ko.bindingHandlers.jqmChangePage = {
    'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        $(element).on('click', function() {
            var page = valueAccessor();
            $.mobile.changePage(page);
        });
    }
}

Este custom binding es muy simple. Durante su inicialización (que realiza Knockout al aplicar los bindings a la página), añade un manejador para el evento click del elemento html sobre el que se aplica y, en respuesta al click, usa la función $.mobile.chagePage de jQuery Mobile para cambiar a la página indicada.

Editar un arma

Los bindings necesarios para la edición de un arma se configuran de la siguiente forma:

<div data-role="page" id="edit" data-bind="jqmWith: selectedWeapon">
    <div data-role="header">
        <a href="#" data-rel="back" data-role="button" data-icon="back" data-theme="a">Back</a>
        <h1 data-bind="text: name"></h1>
    </div>
    <div data-role="content">
        <div data-role="fieldcontain">
            <div>
                <label for="weight">Peso</label>
                <input type="number" id="weight" data-bind="value: weight"/>
            </div>
            <div>
                <label for="ammoType">Munición</label>
                <select id="ammoType" data-bind="options: ammoTypes, value: ammoType"></select>
            </div>
            <div>
                <label for="closeCombatRating">Efectividad cuerpo a cuerpo</label>
                <div id="closeCombatRating" data-bind="starRating: closeCombatRating"></div>
            </div>
            <div>
                <label for="rangeRating">Efectividad a distancia</label>
                <div id="rangeRating" data-bind="starRating: rangeRating"></div>
            </div>
        </div>
    </div>
</div>

Esta pantalla no está enlazada al ViewModel principal de la aplicación. Cuando en la parte anterior del tutorial veíamos la estructura de ViewModels, teníamos algo así:

ViewModels de W.I.Z.A.R&D

ViewModels de W.I.Z.A.R&D

Para indicar a Knockout que los enlaces de esta pantalla son relativos a selectedWeapon en lugar de a MainViewModel, se debe utilizar el un binding de tipo with. Sin embargo, esto presenta problemas con jQuery Mobile. Al usar un binding de tipo with, Knockout vuelve a generar el html cada vez que se refresca la propiedad enlazada, y eso choca con la reescritura del html que hace jQuery Mobile para dotarlo del aspecto y la funcionalidad propias de una aplicación móvil.

La forma de solucionar esto es recurrir a un custom binding que se encargue de obligar a jQuery Mobile a refrescar el html cada vez que Knockout lo reescribe:

ko.bindingHandlers.jqmWith = {
    'init': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        return ko.bindingHandlers["with"].init(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
    },
    'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var t = ko.bindingHandlers["with"].update(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext);
        setTimeout(function () {
            // rebuild styles after dom is created
            $(element).trigger("pagecreate");
        }, 0);
        return t;
    }
};

Este custom binding actúa como un decorador sobre el with original de Knockout, pero cuando se produce una actualización y Knockout invoca el método update, después de dejar que el with original haga su trabajo se dispara el evento pagecreate sobre el elemento html para que jQuery Mobile reescriba el html.

Aparte de esto, en esta pantalla podemos ver también cómo enlazar controles que van a actualizar el modelo cuando el usuario interactúe con ellos, como el caso de listas de selección o cuadros de texto:

<!-- Enlace a una lista de selección -->
<select id="ammoType" data-bind="options: ammoTypes, value: ammoType"></select>
<!-- Enlace a un cuadro de texto -->
<input type="number" id="weight" data-bind="value: weight"/>

El último punto destacable es el custom binding usado para valorar la efectividad de una arma. Este binding muestra la típica lista de estrellas para indicar una valoración y su código es el siguiente:

ko.bindingHandlers.starRating = {
    init: function(element, valueAccessor) {
        $(element).addClass("starRating");
        for (var i = 0; i < 5; i++)
           $("").appendTo(element);
       
        $("span", element).each(function(index) {
            $(this).hover(
                function() { $(this).prevAll().add(this).addClass("hoverChosen", 750) },
                function() { $(this).prevAll().add(this).removeClass("hoverChosen", 750) }                
            ).click(function() {
                var observable = valueAccessor();  
                observable(index+1);
            });
        });            
    },
    update: function(element, valueAccessor) {
        var observable = valueAccessor();
        $("span", element).each(function(index) {
            $(this).toggleClass("chosen", index < observable());
        });
    }    
};

El código está sacado del tutorial de Knockout y, realmente, ni siquiera es el mejor interfaz de usuario para un dispositivo móvil, pero quería incluirlo como ejemplo de custom binding que modifica propiedades del modelo. Básicamente está usando jQuery para, durante la inicialización del binding, añadir 5 spans y asignar los manejadores de eventos adecuados. Al refrescar el binding modifica las clases css asociadas a cada span para reflejar el valor de la propiedad.

Añadir un arma

Por último, los bindings del diálogo de añadir arma son los siguientes:

<div data-role="dialog" id="addNew" data-bind="jqmWith: newWeapon">
    <div data-role="header">
        <h1>Añadir Arma</h1>
    </div>
    <div data-role="content">
        <label for="newWeaponName">Nombre:</label>
        <input type="text" name="newWeaponName" data-bind="value: name"></input>
   
        <fieldset class="ui-grid-a">
           <div class="ui-block-a"><a href="#" data-rel="back" data-role="button" data-bind="click: clear">Cancelar</a></div>
           <div class="ui-block-b"><a href="#" data-rel="back" data-role="button" data-theme="b" data-bind="click: add">Aceptar</a></div>
        </fieldset>
    </div>
</div

Los bindings de esta pantalla son más de lo mismo y no aportan nada nuevo con respecto a las dos anteriores.

Poniendo en marcha el data binding

Con esto tendríamos configurado el enlace entre la vista y el ViewModel. Para hacer efectivo ese enlace, necesitamos instanciar un objeto con el modelo y usar a función ko.applyBindings:

$(function(){
    var viewModel = new MainViewModel();
    ko.applyBindings(viewModel);
});

Conclusiones y temas pendientes

W.I.Z.A.R&D es una aplicación muy sencilla, pero nos ha permitido ver bastantes cosas sobre cómo funcionan jQuery Mobile y Knockout, además de resolver algunos problemas que surgen al integrarlos, como el caso del binding with que veíamos un poco más arriba.

Al tratarse de una introducción, hemos dejado unas cuantas cosas por el camino en las que puede merecer la pena profundizar un poco:

  • Persitencia del ViewModel. Ahora mismo cuando se recarga la página se pierden todos los datos, cosa poco deseable en la vida real. Para solucionar esto podríamos sincronizar la información con un servidor remoto mediante AJAX o utilizar almacenamiento local del navegador, como WebSQL o WebStorage.
  • No hemos escrito ni un sólo test. Sigo teniendo pendiente jugar un poco más con QUnit y ver qué partido se le puede sacar.

El código completo del tutorial lo tenéis en su repositorio de github y también podéis ver la demo online de W.I.Z.A.R&D. Si os animáis, nada mejor que crear vuestro propio fork como ya hicieron otros con el tutorial de node.js + express + jQuery y experimentar un poco.

2 comentarios en “Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding

  1. Pingback: Tutorial jQuery Mobile + Knockout (III): Definiendo el ViewModel con Knockout « Koalite's blog

  2. Hola gracias por la ayuda, sabes estaba probando tu ejemplo pero veo unos errores así TypeError: e.data(«page») is undefined, a qué crees que se deba?

Comentarios cerrados.