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 (esto que estás leyendo)
- Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding
Definiendo el ViewModel
Después las dos primeras partes de este tutorial en las que presentamos jQuery Mobile y Knockout y creamos las vistas con jQuery Mobile ha llegado el momento de definir el ViewModel
de nuestra pequeña aplicación.
En la primera parte del tutorial explicaba que el ViewModel
es el encargado de adaptar el modelo de la aplicación (el dominio en terminología DDD) a las necesidades de la vista. Además contiene toda la lógica relacionada con el interfaz de usuario, como por ejemplo cuándo habilitar un control o qué hacer cuando se pulsa un botón.
Teniendo clara esa idea, veamos cómo debería ser el ViewModel
de nuestra aplicación. Recordemos primero cómo era W.I.Z.A.R&D:

Pantallas de W.I.Z.A.R&D
Para cada pantalla de la aplicación tendremos un ViewModel
diferente. Esto no es estrictamente necesario, aunque suele ser lo más habitual para evitar crear un único ViewModel
excesivamente complejo.
Definiendo el ViewModel
En esta pequeña aplicación, necesitamos poder cubrir las siguientes funcionalidades:
- Mostrar una lista con todas las armas.
- Mostrar el peso acumulado de todas las armas.
- Editar el arma sobre el que pulsamos.
- Eliminar el arma sobre el que pulsamos.
- Añadir un nuevo arma
Para ello, vamos a montar la siguiente estructura de ViewModel
s:

ViewModels de W.I.Z.A.R&D
Tendremos un MainViewModel
que servirá como ViewModel
principal de la aplicación. Desde él se expondrán las propiedades y métodos necesarios para la pantalla con la lista de armas.
Para la edición de un arma, se usara EditWeaponViewModel
, que incluye toda la información asociada a un arma. Por mantener el ejemplo sencillo, vamos a usar instancias de EditWeaponViewModel
para almacenar en MainViewModel
la lista de armas. En un caso más real, lo lógico sería usar distintos tipos de objetos, ya que para mostrar la lista no necesitamos toda la información que aparece en EditWeaponViewModel
y, en el momento en que se vaya empezar a editar un arma, se podría acceder al servidor para descargar toda la información necesaria.
Por último, para añadir un arma usaremos NewWeaponViewModel
.
El código usado para implementar todo esto es el siguiente:
function MainViewModel() { var self = this; self.weapons = ko.observableArray(); self.weapons.push(new EditWeaponViewModel('Martillo', 1, 'Ilimitada', 4, 0)); self.weapons.push(new EditWeaponViewModel('Machete', 1, 'Ilimitada', 5, 0)); self.weapons.push(new EditWeaponViewModel('Pistola', 1, 'Frecuente', 0, 3)); self.weapons.push(new EditWeaponViewModel('Carabina', 3, 'Normal', 0, 4)); self.newWeapon = new NewWeaponViewModel(self.weapons); self.selectedWeapon = ko.observable(); self.remove = function() { // Aquí, knockout hace que "this" sea un EditWeaponViewModel self.weapons.remove(this); }; self.edit = function() { // Aquí, knockout hace que "this" sea un EditWeaponViewModel self.selectedWeapon(this); }; self.totalWeight = ko.computed(function() { var total = 0; ko.utils.arrayForEach(self.weapons(), function(w) { total = total + w.weight(); }); return total; }, this); }; function EditWeaponViewModel(name, weight, ammoType, closeCombatRating, rangeRating) { this.name = ko.observable(name); this.weight = ko.numericObservable(weight || 0); this.ammoType = ko.observable(ammoType || 'Ilimitada'); this.closeCombatRating = ko.observable(closeCombatRating || 0); this.rangeRating = ko.observable(rangeRating || 0); this.ammoTypes = ['Ilimitada', 'Frecuente', 'Normal', 'Rara']; }; function NewWeaponViewModel(existingWeapons) { var self = this; self.name = ko.observable(''); this.clear = function() { self.name('') }; this.add = function() { existingWeapons.push(new EditWeaponViewModel(self.name())); self.clear(); }; }
Pese a lo sencillo del ejemplo, nos permite ver unos cuantos conceptos básicos a la hora de crear modelos con Knockout:
- Un
ViewModel
es simplemente una función usada para construir objetos en Javascript (me resisto a llamarlo clase porque no lo es). - Las propiedades que van a enlazarse usando data biding se encapsulan en objetos
ko.observable()
. Sería el equivalente a implementar INotifyPropertyChanged en .NET, ya que permite a Knockout detectar los cambios en la propiedad para aplicarlos a los controles a los que está enlazada. Hay que tener en cuenta que dejan de ser propiedades «normales» y deben usarse de forma un poco especial:- Para asignarles un valor:
model.name('ruperta')
- Para leer el valor que contienen:
var n = model.name()
- Para asignarles un valor:
- Para definir una lista que queremos enlazar con data binding, debemos encapsularla en un objeto
ko.observableArray()
, que no es más que un observable que encapsula un array. El equivalente en .NET sería INotifyCollectionChanged. - Podemos tener propiedades observables que dependen de otras propiedades, como
totalWeight
. Para definirlas se usa la funciónko.computed()
, que en su versión más simple recibe como parámetro la función usada para el cálculo del valor de la propiedad. Knockout detecta automáticamente el grafo de dependencias para actualizar el control asociado a esta propiedad cuando sea necesario (en este caso, cada vez que cambia el array de armas o el peso de un arma). - Las acciones que se pueden realizar quedan representadas con métodos del
ViewModel
, como los casos deedit
yremove
. Es importante tener en cuenta que, al usar estos métodos asociados al binding de una lista, Knockout es lo bastante listo como para invocarlos sobre el elemento actual de la lista. Simplificando, que al invocar el métodoedit
oremove
,this
será una referencia al elemento deweapons
que vamos a eliminar.
Es también importante destacar que este código no tiene ninguna dependencia sobre la vista, no referencia ningún elemento html ni utiliza APIs relacionadas directamente con la vista. Esto más que una característica de Knockout es algo genéricos al patrón MVVM, pero merece la pena recordarlo.
Un aspecto especialmente interesante del código anterior es la forma en que se declara el peso (weight
) de cada arma. Por defecto, al enlazar una propiedad a un elemento html, Knockout va a realizar el enlace como si fueran string
s. Sin embargo, el peso queremos almacenarlo como un float
para que luego se pueda sumar correctamente en la propiedad calculada totalWeight
.
Para cambiar la forma en que se enlaza una propiedad se puede definir un nuevo tipo de observable, que en este caso llamaremos ko.numericObservable
:
ko.numericObservable = function(initialValue) { var _actual = ko.observable(initialValue); return ko.computed({ read: function() { return _actual(); }, write: function(newValue) { var parsedValue = parseFloat(newValue); _actual(isNaN(parsedValue ) ? newValue: parsedValue); } }); };
Estamos añadiendo al objeto ko
una nueva función, numericObservable
que internamente se encarga de crear una propiedad calculada, similar a la del peso total que vimos un poco más arriba, pero que además hace la conversión de string
a float
al escribir sobre ella, permitiéndonos así mantener su valor interno como float
.
Podríamos haber hecho esto dentro del ViewModel
, exponiendo una propiedad formattedWeight
y traduciendo en el propio ViewModel
, pero esta solución resulta más elegante y reutilizable. Otra alternativa (quizá más correcta) hubiera sido emplear un custom binding, pero como eso lo veremos para otras cosas en el próximo post, he preferido usar este método para ver más opciones.
Próximamente…
Ya tenemos creadas las vistas y el ViewModel
de la aplicación, pero todavía nos falta engancharlo todo para que la aplicación haga algo interesante. En la próxima parte de este tutorial veremos como hacer esto y cómo solucionar algunos problemas que surgen al mezclar jQuery Mobile con Knockout, pero no nos adelantemos.
De todas formas, si quieres ver el código completo del tutorial lo puedes encontrar ya en su repositorio de github o acceder directamente a la demo online de W.I.Z.A.R&D.
Enhorabuena por el ejemplo. Si es posible una consulta. Todo se implenta en el index.html en una pagina interna y los datos que añades se van guardando. Si se introduce en una aplicación con paginas externas, los datos se pierden al pasar de una a otra. Como se puede solucionar?.
Muchas gracias.
Tendría que revisarlo, pero creo recordar que si usas jQueryMobile y navegas entre páginas usando $.mobile.changePage(), las páginas «externas» las va a crear como div’s dentro de la página inicial, por lo que los datos seguirían siempre accesibles.
No obstante, en una aplicación real, los datos no estarían almacenados en la propia página porque se perderían al recargarla. En ese caso, podrías usar websql (para almacenar en el lado del cliente) o almacenar directamente en un servidor y comunicar las páginas a través de él. Algo así como:
– Cuando en la página de editar se terminan de hacer cosas, su ViewModel envía el resultado de los cambios al servidor.
– Cuando se activa la página de la lista, lo primero que hace es refrescar los elementos de la lista con una petición al servidor.
Pingback: Tutorial jQuery Mobile + Knockout (y IV): Configurando el Data Binding « Koalite's blog
Pingback: Tutorial jQuery Mobile + Knockout (II): Creando las vistas con jQuery Mobile « Koalite's blog