Este post forma parte de una serie de cuatro:
- Tutorial jQuery Mobile + Knockout (I): Sentando las bases
- Tutorial jQuery Mobile + Knockout (II): Creando las vistas con jQuery Mobile
- Tutorial jQuery Mobile + Knockout (III): Definiendo el ViewModel con Knockout
- Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding (esto que estás leyendo)
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ónedit
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 ViewModel
s, teníamos algo así:

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 span
s 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.
Pingback: Tutorial jQuery Mobile + Knockout (III): Definiendo el ViewModel con Knockout « Koalite's blog
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?