lunes, 27 de mayo de 2013

Knockout.js ... lo que Usted no vió

KnockoutJs banner

Le dediqué un tiempo a conocer más acerca del patrón MVVM . Me agrada, así que se suma a la lista de tecnologías relacionadas con Microsoft que realmente recomiendo. Este es un enfoque propuesto por la compañía que consiste, según sus propios autores, en una especialización del enfoque Modelo Vista Controlador para tecnologías como Windows Presentation Foundation y Silverlight. En la ingeniería de software este tipo de soluciones resulta muy útil para construir interfaces de usuario. Su ámbito de aplicación se ha extendido también al framework ZK (del cual tengo muy buenas referencias) y a HTML5. Incluso existe Google mdv, una variante relacionada con estos temas. En mi caso particular puse mi atención en la implementación para Javascript conocida como KnockuotJs. En esta ocasión no pretendo comentarles las peculiaridades de su uso . Para esto les recomiendo leer este artículo y la documentación de KnockoutJs. Más bien pretendo comentarles algunos detalles que me llamaron la atención al probar los tutoriales.

Dependencias y <select />

KnockoutJs tutorial 1

Al terminar con el primer tutorial debe quedarnos como resultado un modelo más o menos como el que se muestra a continuación.

// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");

    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();    
    }, this);

    this.capitalizeLastName = function() {
        var currentVal = this.lastName();        // Read the current value
        this.lastName(currentVal.toUpperCase()); // Write back a modified value
    };
}

// Activates knockout.js
ko.applyBindings(new AppViewModel());

A continuación se muestra la vista relacionada después de añadir un elemento <select /> conectado con el atributo lastName del modelo.

<!-- This is a *view* - HTML markup that defines the appearance of your UI -->

<p>First name: <strong data-bind="text: firstName">todo</strong></p>
<p>Last name: <strong data-bind="text: lastName">todo</strong></p>
<p>First name: <input data-bind="value: firstName" /></p>
<p>Last name: <input data-bind="value: lastName" /></p>
<p>Full name: <strong data-bind="text: fullName"></strong></p>
<button data-bind="click: capitalizeLastName">Go caps</button>

<p>Last name: 
  <select data-bind="value: lastName" >
      <option value="x">Pérez</option>
      <option>Suárez</option>
      <option value="z">García</option>
      <option>Bertington</option>
      <option>BERTINGTON</option>
  </select>
</p>

En este punto me resultó muy interesante que la librería preserva automáticamente la consistencia del modelo teniendo en cuenta los bindings. Esta es la razón por la cual el botón Go caps solo funciona cuando el apellido es Bertington . La razón es que el binding con el combobox limita el número de valores que se le puede asignar al atributo del modelo. En el ejemplo el valor mencionado es el único que tiene también una opción con las letras mayúsculas.

Las listas y <select /> ... otra vez …

KnockoutJs tutorial 2

Pasando la página (literalmente ;) llegamos al segundo ejemplo que trata sobre los bindings para listas y colecciones de objetos. Lo que se muestra es impresionante y se completa cada paso satisfactoriamente; pero ... hay (al menos) un detalle muy sutil que se escapa. Utilizando Firebug, DragonFly o una herramienta similar es posible constatar que el atributo value de los elementos <option /> utilizados para escoger el menú siempre está asignado a una cadena vacía (al menos en las versiones de Opera, Firefox y Google Chrome que utilicé para probar ...). Con el fin de enviar los datos hacia el servidor es posible que sea útil asignar ciertos identificadores. Para lograr ese objetivo en la versión final es posible añadir en el model el atributo mealId

// Class to represent a row in the seat reservations grid
function SeatReservation(name, initialMeal) {
    var self = this;
    self.name = name;
    self.meal = ko.observable(initialMeal);

    self.formattedPrice = ko.computed(function() {
        var price = self.meal().price;
        return price ? "$" + price.toFixed(2) : "None";        
    });
}

// Overall viewmodel for this screen, along with initial state
function ReservationsViewModel() {
    var self = this;

    // Non-editable catalog data - would come from the server
    self.availableMeals = [
        { mealId: "s", mealName: "Standard (sandwich)", price: 0 },
        { mealId: "p", mealName: "Premium (lobster)", price: 34.95 },
        { mealId: "u", mealName: "Ultimate (whole zebra)", price: 290 }
    ];    

    // Editable data
    self.seats = ko.observableArray([
        new SeatReservation("Steve", self.availableMeals[0]),
        new SeatReservation("Bert", self.availableMeals[0])
    ]);

    self.addSeat = function() {
        self.seats.push(new SeatReservation("", self.availableMeals[0]));
    }

    self.removeSeat = function(seat) { self.seats.remove(seat) }

    self.totalSurcharge = ko.computed(function() {
       var total = 0;
       for (var i = 0; i < self.seats().length; i++)
           total += self.seats()[i].meal().price;
       return total;
    });
}

ko.applyBindings(new ReservationsViewModel());

Después es necesario utilizar el binding optionsValue en la vista como se muestra a continuación:

<h2>Your seat reservations</h2>

<button data-bind="click: addSeat">Reserve another seat</button>

<table>
    <thead><tr>
        <th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
    </tr></thead>
    <!-- Todo: Generate table body -->
    <tbody data-bind="foreach: seats">
        <tr>
            <td><input data-bind="value: name" /></td>
            <td><select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName', optionsValue: 'mealId'"></select></td>
            <td data-bind="text: formattedPrice"></td>
            <td><a href="#" data-bind="click: $root.removeSeat">Remove</a></td>
        </tr>
    </tbody>
</table>

<h3 data-bind="visible: totalSurcharge() > 0">
    Total surcharge: $<span data-bind="text: totalSurcharge().toFixed(2)"></span>
</h3>

Al aplicar estos cambios se logra generar unos elementos como los siguientes:

<select data-bind="options: $root.availableMeals, value: meal, optionsText: 'mealName', optionsValue: 'mealId'">
  <option value="s">Standard (sandwich)</option>
  <option value="p">Premium (lobster)</option>
  <option value="u">Ultimate (whole zebra)</option>
</select>

¡Urra! ... pero no todo es color de rosa. Como resultado la columna Surcharge deja de actualizarse . Por consiguiente no se muestra el sub-total que se calcula en el último paso. Sin embargo el comando Remove introducido en esa misma etapa sí sigue funcionando. Esto me hace pensar que haya algún problema al calcular alguno de los diferentes tipos de bindings involucrados.

Quizás existe una forma de hacer que vuelvan a funcionar las actualizaciones, pero hasta este momento no la he encontrado. Tampoco he tenido mucho tiempo para profundizar en el tema.

Conclusiones

KnockuotJs es una librería excelente. Sinceramente preferiría que algunos detalles de los bindings fueran más parecidos al estilo de Genshi. De todas maneras los própositos son diferentes; quizás ni siquiera sea apropiado o posible realizarlo.

Me da la impresión de que no voy a poder dejar de utilizarlo después que lo pruebe en alguna aplicación. La recomiendo. Le invito a suscribirse a este blog mediante RSS si desea conocer si descubro algo más en el resto de los tutoriales . En próximos artículos comentaré cómo utilizar esta solución para enriquecer la experiencia de usuario de sus aplicaciones. A pesar de estas pifias los resultados son impresionantes.

No hay comentarios:

Publicar un comentario