Single Page Applications – Parte 5 – Comunicación entre ViewModels

En este post veremos como comunicar las 2 vistas que hemos creado anteriormente(Seleccionar Producto e Ingresar Pedido), de tal manera que cuando seleccionemos un producto en la primera pantalla, desaparezca esta pantalla y se muestre la pantalla de ingreso de pedido con los datos del producto seleccionado.

Para lograr que nuestros 2 viewmodels se comuniquen tenemos las siguientes opciones: crear un objeto padre que se encargue de coordinar todas los view models; utilizar las urls, hasbangs (#!) y routing; o utilizar el patrón publish/suscribe entre las vistas.

En esta oportunidad utilizaremos un objeto padre para coordinar todos los viewmodels del proceso de delivery, pero en los siguientes posts veremos las otras alternativas.

Implementamos este objeto dentro del archivo /Scripts/App/ViewModels/deliveryviewmodel.js, este objeto tendrá una propiedad por cada uno de los viewmodels que esté coordinando.

var DeliveryViewModel = function () {
   
var self = this;

    self.chooseProduct = ko.observable();
    self.placeOrder = ko.observable();
};

Asimismo, este objeto tendrá métodos que serán responsables de mostrar cada una de las vistas del proceso de delivery.

var DeliveryViewModel = function () {
   
var self = this
;

    self.chooseProduct = ko.observable();
    self.placeOrder = ko.observable();

    self.showChooseProduct =
function
() {
       
//TODO: Mostrar la primera vista y ocultar las demás
    };

    self.showPlaceOrder =
function
(productId) {
       
//TODO: Mostrar la segunda vista y ocultar las demás
    };

    self.showConfirmation =
function
() {
       
//TODO: Mostrar la tercera vista y ocultar las demás
    };

    self.showChooseProduct();

};

Los viewmodels ChooseProductViewModel y PlaceOrderViewModel dependerán este nuevo objeto padre y le delegarán la responsabilidad de mostrar la vista adecuada.

var ChooseProductViewModel = function (parent) {
   
var self = this
;
    self.products = ko.observableArray();

    self.goToPlaceOrder =
function
(product) {
        parent.showPlaceOrder(product.id);
    };

    self.init =
function
() {
        ProductsDataSource.getAll(
function (data) {
            self.products(data);
        });
    };

    self.init();
};
var PlaceOrderViewModel = function (productId, parent) {
   
var self = this
;

    self.order = ko.observable();

    self.postOrder =
function
() {
        OrdersDataSource.create(self.order,
function
() {
            parent.showConfirmation();
        });
    };

    self.init =
function
() {
        ProductsDataSource.get(productId,
function
(product) {
            self.order(
new Order(product));
        });
    };

    self.init();
};

El objeto DeliveryViewModel, cada vez que los objetos hijos le delegen mostrar una nueva vista, instanciará el viewmodel adecuado para esta vista.

var DeliveryViewModel = function () {
   
var self = this
;

    self.chooseProduct = ko.observable();
    self.placeOrder = ko.observable();

    self.showChooseProduct =
function
() {
        self.chooseProduct(
new
ChooseProductViewModel(self));
        self.placeOrder(
null
);
    };

    self.showPlaceOrder =
function
(productId) {
        self.chooseProduct(
null
);
        self.placeOrder(
new
PlaceOrderViewModel(productId, self));
    };

    self.showConfirmation =
function
() {
       
//TODO: Mostrar la Tercera vista y ocultar las demás
    };

    self.showChooseProduct();
};

Podemos observar que para ocultar una vista simplemente asignamos null a la propiedad, esto se debe a que Knockoutjs automáticamente eliminará toda la sección HTML si es que el valor del binding es null.

Necesitamos enlazar toda la aplicación al nuevo objeto DeliveryViewModel, para esto modificamos el archivo /Scripts/application.js donde se inicializan los viewmodels.

function initializeApplication() {
    initializeViewModels();
}

function
initializeViewModels() {
    ko.applyBindings(
new DeliveryViewModel());
}

Enlazamos cada vista de manera individual a una propiedad del DeliveryViewModel, para esto modificamos el archivo /Views/Home/Index.cshtml.

<div data-bind="with: chooseProduct">
    @Html.Partial("_ChooseProduct")
</div>

<div data-bind="with: placeOrder">
    @Html.Partial("_PlaceOrder")
</div>

<script type="text/javascript">
    $(function
() {
        initializeApplication();
    });

</script
>

Por último agregamos las referencias a todos los nuevos scripts dentro del archivo /Views/Shared/Layout.cshtml.

<script src="~/Scripts/Lib/jquery-1.6.2.min.js" type="text/javascript"></script>
<
script src="~/Scripts/Lib/knockout-2.1.0.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Data/productsdatasource.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Data/ordersdatasource.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Models/order.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/ViewModels/deliveryviewmodel.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/ViewModels/chooseproductviewmodel.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/ViewModels/placeorderviewmodel.js" type="text/javascript"></script
>
<
script src="~/Scripts/application.js" type="text/javascript"></script>

Si ejecutamos la aplicación podremos ver el listado de productos.

ChooseProd_Small

Si seleccionamos alguno de los productos aparecerá la pantalla para completar los datos de la orden.

placeorder

En el siguiente post veremos la tercera pantalla del proceso de delivery.

Saludos
Angel Núñez Salazar

Single Page Applications – Parte 4 – Más KnockoutJS

En este post vamos a implementar la segunda pantalla de nuestra panadería online.

Pantalla: Ingresar Pedido

Pantalla a través de la cuál un usuario ingresa los datos del pedido para que este sea enviado a la dirección especificada.

PlaceOrder

Creamos el ViewModel para esta pantalla dentro del archivo /Scripts/App/ViewModels/placeorderviewmodel.js. Este ViewModel recibirá como parámetro el código del producto seleccionado para el pedido.

var PlaceOrderViewModel = function (productId) {
   
var self = this
;

    self.order = ko.observable();

    self.postOrder =
function
() {
        OrdersDataSource.create(self.order,
function
() {
           
//TODO: Ir a la pantalla de confirmación
        });
    };

};

Delegamos la creación de la orden al objeto OrdersDataSource y lo implementamos dentro del archivo /Scripts/App/Data/ordersdatasource.js.

var OrdersDataSource = (function () {
   
return
{
        create:
function
(order, success) {
            $.ajax({
                type:
"POST"
,
                url:
"/api/orders"
,
                dataType:
"json"
,
                contentType:
"application/json;charset=utf-8",
                data: ko.toJSON(order),
                success: success
            });
        }
    };
} ());

Completamos el código del PlaceOrderViewModel para cargar los datos del producto que serán mostrados en el formulario de la orden.

var PlaceOrderViewModel = function (productId) {
   
var self = this
;

    self.order = ko.observable();

    self.postOrder =
function
() {
        OrdersDataSource.create(self.order,
function
() {
           
//TODO: Ir a la pantalla de confirmación
        });
    };

    self.init =
function
() {
        ProductsDataSource.get(productId,
function
(product) {
           
//TODO: Crear una instancia de la Orden con los datos del producto
        });
    };

    self.init();

};

Agregamos un nuevo método dentro del objeto ProductsDataSource.

var ProductsDataSource = (function () {
   
return
{
        getAll:
function
(success) {
            $.ajax({
                type:
"GET"
,
                url:
"/api/products"
,
                contentType:
"application/json;charset=utf-8"
,
                dataType:
"json"
,
                success: success
            });
        },
        get:
function
(id, success) {
            $.ajax({
                type:
"GET"
,
                url:
"/api/products/"
+ id,
                contentType:
"application/json;charset=utf-8"
,
                dataType:
"json"
,
                success: success
            });
        }

    };
} ());

Creamos la clase Order  dentro del archivo /Scripts/App/Models/order.js, esta clase contendrá los datos del pedido que serán enviados al servicio REST.

var Order = function (product) {
   
var self = this
;
    self.address = ko.observable();
    self.email = ko.observable();
    self.quantity = ko.observable(1);
    self.productId = product.id;
    self.product = product;
    self.total = ko.computed(
function
() {
       
return (self.product.price * self.quantity()).toFixed(2);
    });
}

Utilizamos la sentencia ko.observable() para relacionar las propiedades de esta clase con los campos del formulario. También utilizamos la sentencia ko.computed() para crear una función observable que dependa de otros observables, esto permitirá que el monto total de la orden se actualice automáticamente cuando el valor de la cantidad cambie.

Completamos el código del PlaceOrderViewModel para instanciar una nueva orden apenas se reciba los datos del producto.

var PlaceOrderViewModel = function (productId) {
   
var self = this
;

    self.order = ko.observable();

    self.postOrder =
function
() {
        OrdersDataSource.create(self.order,
function
() {
           
//TODO: Ir a la pantalla de confirmación
        });
    };

    self.init =
function
() {
        ProductsDataSource.get(productId,
function
(product) {
            self.order(
new Order(product));
        });
    };

    self.init();
};

Creamos la vista parcial /Views/Home/_PlaceOrder.cshtml y agregamos el código HTML correspondiente esta pantalla.

<!-- ko with: order -->
<h1 data-bind
="text: 'Place Your Order: '+product.name">
</
h1
>
<
form action="" method="post">
    <fieldset class="no-legend">
        <legend>Place Your Order</legend>
        <img class="product-image order-image"
 
            
data-bind=
"attr:{ src:'/content/images/products/thumbnails/' + product.imagename,
                        alt:product.name }"
 />
        <ol>
            <li>
                <label for="orderEmail">Your Email Address</label>
                <input type="text" id="orderEmail" name="orderEmail" data-bind="value:email" />
            </li>
            <li>
                <label for="orderShipping">Shipping Address</label>
                <textarea rows="4" cols="20" id="orderShipping" name="orderShipping"
 
                         
data-bind="value:address">
                </textarea>
            </li>
            <li class="quantity">
                <label for="orderQty">Quantity</label>
                <input type="text" id="quantity" name="quantity"
 
                      
data-bind="value:quantity, valueUpdate: 'afterkeydown'" />
                x <span id="orderPrice" data-bind="text:product.price"></span>
                = <span id="orderTotal" data-bind="text:total()"></span></li>
        </ol>
        <p>
            <input type="submit" value="Place Order" data-bind="click:$parent.postOrder" />
        </p>
    </fieldset
>
</
form>
<!-- /ko –>

Examinemos brevemente los nuevos “bindings” que estamos utilizando.

  • with: Crea un “contexto” dentro del cuál todos los bindings hacen referencia al objeto asignado. Una característica de este binding es que lo podemos utilizarlo dentro de un comentario sin la necesidad de crear un nuevo tag.
  • value: Asocia el atributo “value” con el valor de una propiedad dentro del ViewModel. Típicamente usado con elementos dentro de un formulario como: <input>, <select>, <textarea>, etc.
  • valueUpdate: Por defecto los bindings se actualizan cuando se el usuario cambia el foco del elemento. Esta propiedad nos permite controlar cuando se actualizarán los cambios, por ejemplo: “afterkeydown”, “keyup”, “keypress”, etc.

Desde el archivo /Views/Home/Index.cshtml hacemos referencia a la vista parcial recién creada.

<div>
    @Html.Partial("_ChooseProduct");
</div>

<div>
    @Html.Partial("_PlaceOrder");
</div>


<script type="text/javascript">
    $(function
() {
        initializeApplication();
    });

</script
>

Agregamos las referencias de los nuevos scripts dentro del archivo /Views/Shared/Layout.cshtml.

<script src="~/Scripts/Lib/jquery-1.6.2.min.js" type="text/javascript"></script>
<
script src="~/Scripts/Lib/knockout-2.1.0.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Data/productsdatasource.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Data/ordersdatasource.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/Models/order.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/ViewModels/chooseproductviewmodel.js" type="text/javascript"></script
>
<
script src="~/Scripts/App/ViewModels/placeorderviewmodel.js" type="text/javascript"></script
>
<
script src="~/Scripts/application.js" type="text/javascript"></script>

Luego de todos estos cambios, la estructura final de archivos ha quedado de la siguiente manera.

structureSP4

En el siguiente post veremos como comunicar nuestros 2 ViewModels para permitir que la segunda vista recién se muestre luego de seleccionado un producto.

Saludos
Angel Núñez Salazar