knockout.js es una librería javascript que implementa el patrón Model View View-Model.

Patrón Model View View-Model

Patrón Model View View-Model (MVVM) es un patrón diseñado para construir interfaces de usuario. Describe cómo mantener simple una interfaz de usuario sofisticada dividiéndola en tres partes:

Aplicado a Knockout

Ejemplo Simple

http://jsfiddle.net/h7tgN/

En el ejemplo tenemos un campo de texto donde estamos escribiendo en la variable valor. La variable se esta actualizando cuando se presiona una tecla. Después tenemos una etiqueta span donde estamos mostrando el contenido de la variable. Vemos que a medida que vamos escribiendo en el campo de texto se va actualizando la etiqueta automáticamente.

Recomiendo que pruebes los tutoriales de knockout.js http://learn.knockoutjs.com/. Están bastante trabajador y tienen una web donde puedes ir probando el código en la página web directamente y viendo los resultados.

Ejemplo más completo

El ejemplo simple fue únicamente para probar la funcionalidad básica de la aplicación. Este ejemplo es un caso más real: crearemos una tabla de Posts, donde mostraremos el título y el texto. Le añadiremos a la tabla filtrado y edición inline. Con esto veremos cómo utilizar variables observables, dependentObservables y observableArrays. También haremos uso de jquery tmpl para renderizar la vista.

Esqueleto de la página

Enlazamos los css y js que vamos a utilizar:

<!doctype html>
<html>
<head>
    <title></title>

<span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"http://twitter.github.com/bootstrap/1.3.0/bootstrap.min.css"</span><span class="nt">&gt;</span>

<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript "</span> <span class="na">src=</span><span class="s">"https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">src=</span><span class="s">"http://cloud.github.com/downloads/SteveSanderson/knockout/jquery.tmpl.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/javascript"</span> <span class="na">src=</span><span class="s">"http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-1.2.1.js"</span><span class="nt">&gt;&lt;/script&gt;</span>

</head> <body> </body> </html>

Tabla

Añadimos la tabla donde mostraremos el listado de Post en 3 columnas. La primera de selección, la segunda el título y luego el texto.

<div class=“container”>
    <table>
        <thead>
        <tr>
            <th></th>
            <th>Title</th>
            <th>Text</th>
        </tr>
        </thead>
        <tbody />
    </table>
</div>

ViewModel

El viewModel contará con un listado de post que se mostrarán en la tabla

<script>
     function containsIgnoreCase(s, q){
        return s.toLowerCase().indexOf(q.toLowerCase()) != -1;
     }

 <span class="kd">function</span> <span class="nx">Post</span><span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">text</span><span class="p">){</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">title</span> <span class="o">=</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observable</span><span class="p">(</span><span class="nx">title</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">text</span> <span class="o">=</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observable</span><span class="p">(</span><span class="nx">text</span><span class="p">);</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">selected</span> <span class="o">=</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observable</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>

    <span class="k">this</span><span class="p">.</span><span class="nx">containsText</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">query</span><span class="p">){</span>
        <span class="k">return</span> <span class="nx">containsIgnoreCase</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">title</span><span class="p">(),</span> <span class="nx">query</span><span class="p">)</span>
            <span class="o">||</span> <span class="nx">containsIgnoreCase</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">text</span><span class="p">(),</span> <span class="nx">query</span><span class="p">);</span>
    <span class="p">}</span>
 <span class="p">}</span>

 <span class="kd">var</span> <span class="nx">viewModel</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">posts</span> <span class="p">:</span> <span class="nx">ko</span><span class="p">.</span><span class="nx">observableArray</span><span class="p">([])</span>
 <span class="p">}</span>

 <span class="nx">viewModel</span><span class="p">.</span><span class="nx">posts</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">new</span> <span class="nx">Post</span><span class="p">(</span><span class="s1">'title1'</span><span class="p">,</span> <span class="s1">'text1'</span><span class="p">));</span>
 <span class="nx">viewModel</span><span class="p">.</span><span class="nx">posts</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">new</span> <span class="nx">Post</span><span class="p">(</span><span class="s1">'title2'</span><span class="p">,</span> <span class="s1">'text2'</span><span class="p">));</span>

 <span class="nx">ko</span><span class="p">.</span><span class="nx">applyBindings</span><span class="p">(</span><span class="nx">viewModel</span><span class="p">);</span>

</script>

En la clase Post se han definido los 3 campos como variables observables, para que los cambios que se hagan en esas variables se actualicen automáticamente. En el viewModel hemos definido un observableArray con el listado de post. Añadimos a esta lista dos posts de ejemplo.

Para mostrar los posts en la tabla. Creamos una nueva plantilla que mostrará el contenido de cada fila.

<script type=“text/html” id=“postRow”>
    <tr>
        <td><input type=“checkbox” data-bind=“checked: selected” /></td>
        <td>${title}</td>
        <td>${text}</td>
    </tr>
</script>

Y añadimos en binding en la tabla para que renderice la plantilla “postRow” con el listado de “posts”.

    <tbody data-bind=“template : {name : ‘postRow’, foreach: posts}” />

Con este código ya tenemos sincronizada la tabla con la lista de Posts. Para hacer una prueba podemos abrir la consola javascript y añadir un nuevos post.

    viewModel.posts.push(new Post(‘prueba’, ‘desde la consola de javascript!’);

La tabla se tiene que haber actualizado automáticamanete con la nueva fila añadida.

Filtrado

Para añadir filtrado de la tabla vamos a almacenar dos arrays, uno con la lista de posts completos y otro con la lista de posts filtrados.

Añadimos un campo de texto donde vamos a escribir el filtro. El evento por defecto que actualiza el viewModel es cuando pierde foco. Para hacer un filtrado en tiempo real podemos cambiar el evento de actualización a ‘afterkeydown’.

<div class=“container”>

<span class="nt">&lt;form&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"clearfix"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;input</span> <span class="na">class=</span><span class="s">"xxlarge"</span> <span class="na">type=</span><span class="s">"text"</span> <span class="na">data-bind=</span><span class="s">"value : filterQuery, valueUpdate:'afterkeydown'"</span> <span class="na">placeholder=</span><span class="s">"Filter"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/form&gt;</span>

</div>

Modificamos el viewModel para añadir el texto por el que se filtra y la lista de posts filtrados. Definimos la variable filteredPost como dependentObservable, de esta forma, cada vez que se actualice la lista de post, se volverá a evaluar la lista de post filtrados. En el caso de que el filtro esté vacio, mostramos la lista de todos los posts.

var viewModel = {
    posts : ko.observableArray([]),
    filterQuery : ko.observable(‘’)
}

viewModel.filteredPosts = ko.dependentObservable(function(){ var query = this.filterQuery(); if(query){ var filtered = []; $.each(viewModel.posts(), function(i, post){ if(post.containsText(query)){ filtered.push(post); } }); return filtered; } //Not filtering return viewModel.posts(); }, viewModel);

Modificamos el binding de la tabla para mostrar la lista de posts filtrados.

    <tbody data-bind=“template : {name : ‘postRow’, foreach: filteredPosts}” />

Edición inline

Para permitir la edición inline vamos a añadir una nueva variable que nos indique si estamos en modo edición. En el modo de edición, en la tabla aparecerán campos de texto donde el usuario podrá modificar las filas. También añadiremos un botón de nuevo y un botón para borrar las filas seleccionados.

Botones para realizar las acciones.

<div class=“container”>

<span class="nt">&lt;form&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"clearfix"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"btn primary"</span> <span class="na">data-bind=</span><span class="s">"click : newPost"</span><span class="nt">&gt;</span>New post<span class="nt">&lt;/a&gt;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"btn primary"</span> <span class="na">data-bind=</span><span class="s">"visible: editMode, click: toggleEditMode"</span><span class="nt">&gt;</span>Save<span class="nt">&lt;/a&gt;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"btn"</span> <span class="na">data-bind=</span><span class="s">"visible: !editMode(), click : toggleEditMode"</span><span class="nt">&gt;</span>Edit<span class="nt">&lt;/a&gt;</span>
        <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"btn danger"</span> <span class="na">data-bind=</span><span class="s">"click : deletePosts"</span><span class="nt">&gt;</span>Delete<span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/div&gt;</span>
 ...</code></pre></figure>

La plantilla muestra el texto o un input dependiendo del campo editMode.

<script type=“text/html” id=“postRow”>
    <tr>
        <td><input type=“checkbox” data-bind=“checked: selected” /></td>
      
<td><input type=“text” data-bind=“value : title” /></td> <td><input type=“text” data-bind=“value : text” /></td>
<td>${title}</td> <td>${text}</td>
</tr> </script>

Añadimos las acciones al modelo.

var viewModel = {
    posts : ko.observableArray([]),
    filterQuery : ko.observable(‘’),
    editMode : ko.observable(false)
}

viewModel.selectedPosts = ko.dependentObservable(function(){ var result = []; $.each(this.posts(), function(i, post){ if(post.selected()){ result.push(post); } }); return result; }, viewModel);

viewModel.toggleEditMode = function(){ viewModel.editMode(!viewModel.editMode()); }

viewModel.newPost = function(){ viewModel.posts.push(new Post(‘’,‘’)); viewModel.editMode(true); }

viewModel.deletePosts = function(){ viewModel.posts.removeAll(viewModel.selectedPosts()); }

Declarando la variable selectedPosts como dependentObservable nos aseguramos que esté sincronizada con la lista de posts. A la hora de borrar los posts de la lista utilizamos la función removeAll y le pasamos la lista completa de posts seleccionados.

Conclusiones

En el ejemplo hemos visto como podemos utilizar knockout.js para tener sincronizada la interfaz de usuario con el modelo. Haciendo uso de variables dependientes y bindings hemos conseguido darle comportamiento dinámico a una tabla estática. En pocas lineas de código hemos conseguido que nuestra tabla sea completamente editable y filtrable.