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