Mostrando las entradas con la etiqueta jQuery. Mostrar todas las entradas
Mostrando las entradas con la etiqueta jQuery. Mostrar todas las entradas

Primero pasos con Knockout

Luego de varios comentarios positivos sobre este framework y de ver que ya viene incluido en asp.net mvc4, decidí darle un vistazo a knockout 2.0, como resultado de estas pruebas podría decir que knockout me parece muy útil cuando queremos hacer una aplicación web con bastante comportamiento del lado del cliente (browser), es decir, cuando queremos que el armado de la vista (html) sea responsabilidad del cliente (browser) y no tanto del servidor.

¿Que aporta knockout la programación?

Si bien para tener vistas mas dinámicas me alcanzaba con jquery, el aporte del knockout está en no tener que manipular el DOM artesanalmente sino que de eso se encarga knockout en base a los data-bind que nosotros definimos.

¿Que conceptos se agregan respecto de la simple manipulación del DOM?

Se agrega el concepto de view-model observable, uno de los pilares de knockout (sinó "el" pilar).

Ejemplo completo

Descargar

Este ejemplo utiliza mongolab y funciona simplemente abriendo el html desde el filesystem con un browser, no necesita hosting del sitio web (a los fines didáctico).


Para configurar mongolab, siga los pasos descriptos en readme.txt

Conceptos claves

  • Separar correctamente lo que corresponde a la vista de lo que corresponde al view-model
  • Utilizar los data-bind para vicular atributos de html con propiedades del view-model o para iterar como el caso de data-bind="foreach: articles"
  • Todo lo que queremos que se actualice 'automáticamente' debe ser ko.observable(...), ya sean objetos, propiedades, arrays

Analizando algunos fragmentos de código

Para inicializar todos nuestros bindings, tenemos que ejecutar ko.applyBindings luego de que se cargó nuestra página:



Para relacionar un objeto de nuestro view-model con los elementos html debemos utilizar los data-bind, por ejemplo:

donde:

  • El data-bind del form indica que cuando se haga un submit de este form, se ejecute el método add de nuestro view-model y que los data-bind internos del form trabajan con el objeto newItem de nuestro view-model
  • El data-bind del input id="new_name" está indicando que la propiedad relacionada con el value de ese input es name. ¿de quien?, si, de newItem


y por último, veamos como iterar en un array de items:


  • en primer lugar podemos ver la propiedad length bindeada al contenido del elemento span.
  • luego, podemos observar un elemento ul cuyo data-bind nos dice que va a recorrer el array articles de nuestro view-model.
  • ahora bien, dentro de cada li vamos a tener 2 elementos de tipo span que van a estar relacionados con las propiedades name y stock de la misma manera que el span anterior pero con la salvedad que estos están vinculados a cada elemento del array que estamos iterando. También tendremos 2 elementos de tipo button y, como podemos intuir del data-bind cuando se presionen invocarán a los métodos set_current y remove de nuestro view-model (y no del elemento que estamos iterando, para eso es el $parent)


Y... ¿el view-model?

Ahora veremos algunos fragmentos que considero importantes del view-model, pero pueden verlo completo bajándose el ejemplo.


Aquí podemos observar como creamos el view-model (self) creando articles como un array observable y newItem como un objeto observable. Luego definimos length como una propiedad computable.

function articulos() {
    var self = { };
    self.articles = ko.observableArray();
    self.newItem = ko.observable();

    self.length = ko.computed(function() {
        return self.articles().length;
    });


Aquí podemos observar la definición de add, el parámetro f es el form involucrado, las primeras líneas son para validarlo (jquery.validation), luego tomamos el action definido en el form asumiendo que ese es el nombre de la colección a la que queremos agregar el nuevo elemento y llamamos a hacer un $.post con self.newItem() como contenido y, de resultar exitoso el POST, se agrega la respuesta (data) a la colección de articles, previo crearla como observable.

self.add = function(f) {
    var form = $(f);

    var valid = true;
    if (form.valid !== undefined)
        valid = form.valid();

    if (valid === undefined || valid) {
        var collectionName = form.attr("action");

        rest.dopost(collectionName, self.newItem(), function(data) {
            self.articles.push(observableArticulo(data));
            self.reset_newItem();
            self.sort();
        });
    }
    return false;
};


Descargar ejemplo completo

Subpanels

En el anterior post hice una preview (y breve introducción) de una funcionalidad que me ha resultado útil en una aplicación web y espero lo sea en otras. Se trata de subpaneles que permiten agregar elementos durante la carga de otros elementos, por ejemplo permiten agregar Categorías durante el alta de un Libro, o Tiendas durante la carga de una Compra, o Ciudades en Provincias en Países durante la carga de un Cliente, etc.

Algunas características

  • Carga de subforms asincrónica y solo cuando se abre el subpanel (ajax).
  • Eventos para poder enlazar el subpanel con la página contenedora (callbacks).
  • Permite anidar subpaneles (recursive).
  • Integramente escrito con jquery y jquery-ui (client-side).

Usabilidad

Consideremos la siguiente pantalla:
subpanels-6
Cuando configuramos un subpanel, lo único que vemos es que se agrega un botón [+] a la pantalla, como el siguiente:
subpanels-1
Si presionamos este botón [+] se abrirá un subpanel que si estuviese vacío tendría un aspecto similar a:
subpanels-2
pero en nuestro caso tiene el contenido de la url configurada y se ve como el siguiente:
subpanels-5
cuando hagamos el POST del form del subpanel este se procesará, luego se llamará a la función configurada como success (si es que hay una) y, a menos que esta devuelva false, cerrará el subpanel. La función configurada como success recibirá como parámetro la respuesta del POST al servidor y se puede utilizar para mostrar mensajes de validación, actualizar otros controles enlazados al subpanel, etc.

Código

Veamos como logramos estos, considerando la primer imagen de este post, el html contendría algo parecido a lo siguiente:

<body>
  <div class='ui-widget ui-state-default ui-corner-all'>
    <div class='ui-widget-content ui-corner-all'>
      <h1>Compra de libros</h1>
      <form action='/compra' method='POST'>
        <p style="display: inline">Vendedor: 
        <select id="vendedor" name="vendedor" style="vertical-align: top;">
        </select>
        <div id="pnvendedor" style="display: inline"></div></p>
        <p>Fecha: <input id='fecha' name='fecha' type="text" /></p>
        <p style="display: inline">Libro: 
        <select id="libro" name="libro" style="vertical-align: top;">
        </select>
        <div id="pnlibro" style="display: inline"></div></p>
        <p>Cantidad: <input name='cantidad' type="text" /></p>
        <p><input type='submit' value='Guardar'/></p>
      </form>
    </div>
  </div>
</body>
como podemos observar, seguido al elemento select para el combo de vendedor (o de libro) tenemos un div donde se configurará nuestro subpanel. En estos divs se cargaran los botones [+] y [-] que abrirán y cerrarán el panel respectivamente.
para que un div se transforme en un subpanel debemos llamar a la función subpanel(…) como muestra el siguiente código:
$("#pnvendedor").subpanel({ 
  url: 'vendedor.html',
  success: function(data) { addToSelectElement($("#vendedor"), data); },
  imageLoding: '/css/loader.gif'
});
en este código estamos seleccionado el div cuyo id es pnvendedor y transformándolo en un subpanel, cuyo contenido está dado por vendedor.html y se invocará a la función addToSelectElement luego de que el POST del form del subpanel (es decir el subform) sea compleado.

Subpaneles anidados

También podemos anidar subpaneles, en el ejemplo, al abrir el subpanel de Libro tiene otro [+] para agregar Categoría:
subpanels-3
que cuando se presiona en este [+] se abre el otro subpanel  para agregar una Categoría.
subpanels-4

Descargas:

Archivos para usar los subpanels en cualquier sitio (solo js y css)
Archivos del entorno de desarrollo (incluye tests con qunit y servidor con node.js)
Código del ejemplo (incluye html y servidor en node.js)

Subpaneles – Preview

Sistema de subpaneles (o subformulario) que funcionan del lado del browser comunicándose mediante request asincrónicos al servidor (ajax). Originalmente fue desarrollado y usado en el sitio de la comunidad alt.net hispano. Luego generalizado en este ejemplo, testeado con qunit y con node.js del lado del servidor.
Unable to display content. Adobe Flash is required.
El código es algo como:
$("#pnvendedor").subpanel({
                    successful: function(data) { addToSelectElement(data, $("#vendedor")); },
                    imageLoding: "/css/loader.gif"
                    url: "vendedor.html" 
                });
donde pnvendedor es un contendor (div) donde se ubicarán los botones [+] y [–], por ejemplo:
<div id="pnvendedor" style="display: inline"></div>

Ir al post de subpanels

Drag and drop entre listas con asp.net mvc y jQuery

View

Para hacer la funcionalidad de drag and drop entre listas con jquery basta con una simple instrucción:

$("#left #center #right").sortable({
                connectWith: '.drag-and-drop-connectedSortable'
            }).disableSelection();

Aquí le estamos diciendo que los elementos left, center y right (en mi caso son todas <ul>) son listas conectadas con los elementos que tengan como class a “drag-and-drop-connectedSortable” (que en este caso son las mismas listas), lo que permite tomar elementos de una lista y ponerlos en otra.

Luego agregamos algunos detalles estéticos y logramos algo como:

ui

los nombres en este ejemplo son en agradecimiento a los que participar en la discusión del foro sobre algunos temas relacionados (hasta el momento de la publicación de este post). hilo en altnet-hispano.

Controller

La acción del controller que deseo invocar es la siguiente:

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult SendList(IList<string> left, IList<string> center, IList<string> right)
{
    return Json(new
                    {
                        Right = from i in right select i.ToLower(),
                        Center = from i in center select i.ToLower(),
                        Left = from i in left select i.ToLower()
                    });
}

pero al momento de invocar dicha acción para informarle sobre los elementos seleccionados en cada lista me encontré con algunos inconvenientes que conversamos en altnet-hispano (hilo):

  • Pasar una lista (<ul> <li>) al controller como parámetros de una acción.
  • Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.
Pasar una lista (<ul> <li>) al controller como parámetros de una acción

Aquí vimos que no era posible que asp.net mvc interpretara la lista como valores que el usuario puede modificar ya que como dijo Fernando solo los <input…> son tenidos en cuenta. Con esto ya decidimos hacer la invocación del POST con jquery.

Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.

Para esto tuvimos agregar la línea: jQuery.ajaxSettings.traditional = true; ya que sino asp.net mvc no puede interpretar correctamente los parámetros de tipo array.

El código

$(document).ready(function() {
    jQuery.ajaxSettings.traditional = true;

    $("#form").submit(function() {
        var leftvalues = [];
        $("#left li").each(function() { leftvalues.push($(this).text()) });
        var centervalues = [];
        $("#center li").each(function() { centervalues.push($(this).text()) });
        var rightvalues = [];
        $("#right li").each(function() { rightvalues.push($(this).text()) });

        var postData = { left: leftvalues, center: centervalues, right: rightvalues };

        $.post('<%= Url.Action("SendList", "DragAndDrop") %>',
            postData,
            function(data) {
                alert("left: "+ data.Left + "\n"+ "center: "+ data.Center + "\n"+ "right: "+ data.Right);
            },
            'json');

        return false;
    });

Cómo comentaba mas arriba, la primera línea es debido a que estamos usando jQuery 1.4.x por lo que la serealización de parámetros de tipo array es diferente a como la espera asp.net mvc, por eso seteamos el modo tradicional. les dejo un link al respecto: http://forum.jquery.com/topic/jquery-1-4-breaks-asp-net-mvc-parameter-posting

Luego, simplemente estamos tomando los valores de las listas, pasándolos a un array y armando el post.

Código completo

http://dl.dropbox.com/u/7345566/MvcAndjQuery.7z

Proceso asincrónico en asp.net mvc con jquery

Lo que vamos a hacer en esta oportunidad es una aplicación asp.net mvc con un proceso de duración considerable como para querer tener una barra de progreso usando jquery y jquery.ui

Básicamente, esto se logra haciendo llamadas asincrónicas (e independientes) desde la view a las acciones del controller.

mvc

Controller

En el controller vamos a tener dos acciones, una que se va a invocar mediante un POST (el proceso principal) y otra que se va a invocar mediante otro POST y va a devolver un JSON (consulta del estado del proceso).

Nota: Vamos a hacer uso de los items de HttpApplication para guardar el estado del mismo y para poder ejecutar varios procesos en forma simultanea es que los mismos tienen un processId. Otra opción sería, por ejemplo, persistirlo en una base de datos.

Proceso principal
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LongProcess(int loops, string processId)
{
    HttpContext.Application["Status_" + processId] = 0;
    HttpContext.Application["Loops_" + processId] = loops;

    for (int i = 0; i < loops; i++)
    {
        Thread.Sleep(1000); //Action

        HttpContext.Application["Status_" + processId] = i + 1; //Update status
    }
    return View("Result", new ResultModel {Loops = loops});
}

donde Thread.Sleep(1000) está representando la duración de una etapa del proceso.

Consulta del estado del proceso

aquí tomamos los valores del estado del proceso, calculamos el porcentaje y armamos una respuesta JSON.

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GetStatus(string processId)
{
    int status = HttpContext.Application["Status_" + processId] != null
                    ? (int)HttpContext.Application["Status_" + processId]
                    : 0;
    int loops = HttpContext.Application["Loops_" + processId] != null
                    ? (int)HttpContext.Application["Loops_" + processId]
                    : 0;

    int value = 0;
    if (status != 0 && loops != 0)
        value = status * 100 / loops;

    return Json(new { Result = value });
}

View

La vista propiamente dicha contiene un div con los controles que realizan la invocación:

  • un formulario: fromprocess
  • un hidden para guardar el Id de proceso: processId
  • otro hidden para guardar el Id del intervalo que consulta el estado periódicamente: intervalId
  • una caja de texto para ingresar la cantidad de ciclos del loop que estamos simulando: loops.
  • un botón de tipo submit para invocar al post del formulario.

y otro div para mostrar el progreso, el estado y el resultado.

<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<% using (Html.BeginForm("LongProcess", "Home", FormMethod.Post, new {id = "fromprocess"}))
   {%>
    <div id="invoke">
        <fieldset>
            <%=Html.Hidden("processId", Model.ProcessId, new { id = "processId" })%>
            <input id="intervalId" type="hidden" />
            <%= Html.TextBox("loops", 10, new {id = "loops"}) %>
            <input type="submit" value="Run Long process" />
        </fieldset>
    </div>
    <div id="progress">
        <div id="progressbar"></div>
        <div id="status"></div>
        <div id="result"></div>
    </div>
<% } %>

La función javascript para consultar el estado

Esta función consulta el estado del proceso haciendo un POST e interpretando el JSON de respuesta. En url indicamos el link a consultar, en data los parámetros de la acción (en el controller) y cuando la consulta se completa tomamos el resultado (que ya viene parseado a JSON), asignamos la propiedad Result al progressbar y en texto al div status.

function updateProgress() {
    $.ajax({
        url: '<%=Url.Action("GetStatus", "Home") %>',
        data: { processId: $("#processId").val() },
        success: function(data) {
            if (data != null) {
                $("#progressbar").progressbar("option", "value", data.Result);
                $("#status").html("Progress: " + data.Result + "%");
            }
        },
        error: function(msg) {
            $("#status").html("Error: " + msg);
        }
    });
}
La función javascript para hacer el POST asincrónico

cuando la página se carga estamos inicializando algunas propiedades…

  • en la primera línea estamos configurando el div progressbar como una progressbar de jquery.
  • en la segunda estamos ocultando el div progress (será visible cuando se inicie el proceso).
  • la tercer línea es muy importante para que funcione correctamente en IE, sino IE hace un cache de las llamadas ajax y nunca vuelve a consultar el estado al servidor sino que se queda con la primer respuesta. (para FF y Chrome no es necesaria).
  • en la cuarta línea estamos agregando una función al submit del formulario de nuestra página que va a realizar la misma invocación que haría el formulario pero en forma asincrónica.
$(document).ready(function() {
    $("#progressbar").progressbar({ value: 0 });
    $("#progress").hide();

    $.ajaxSetup({ cache: false }); // this line is needed to ajax on IE

    $("#fromprocess").submit(function() {
        $("#invoke").hide(1000);

        $("#progressbar").progressbar("option", "value", 0);
        $("#status").html("loading...");
        $("#progress").show(1000);

        $.ajax({
            type: "POST",
            url: $("#fromprocess").attr("action"),
            data: $("#fromprocess").serialize(),
            success: function(data) {
                clearInterval($("#intervalId").val());
                $("#progressbar").progressbar("option", "value", 100);
                $("#status").html("complete");
                $("#result").html(data);
            },
            error: function(msg) {
                clearInterval($("#intervalId").val());
                $("#status").html("error");
                $("#result").html(msg);
            }
        });

        var intervalId = setInterval("updateProgress()", 100);
        $("#intervalId").val(intervalId);

        return false;
    });
});

la función que acabamos de poner en el submit hace una invocación ajax de tipo POST a url que tiene configurada como action el form fromprocess, en data envía el formulario serializado, esto hace que no nos quedemos esperando el resultado del post sino que indicamos una función js que se va a invocar cuando el proceso se complete.

Luego seteamos un intervalo de ejecución para la función updateProgress y guardamos el Id del intervalo para, que cuando se complete el proceso, detenerlo.

Cuando se completa el proceso:

  • detenemos la consulta de estado
  • seteamos el progressbar en 100%
  • ponemos el status en complete
  • mostramos el resultado en el div result.

Código completo

para descargar el código completo, click aquí