knockout.js
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:
- Model: Los datos de la aplicación.
- View-Model : Representación de los datos y las operaciones de la interfaz gráfica. No es la interfaz de usuario en sí, no tiene los conceptos de botones o estilos.
- Vista : Representación gráfica del estado del View-Model. Muestra la información del View-Model y envía comandos para ejecutar las acciones.
Aplicado a Knockout
- Modelo : Normalmente llamadas Ajax para leer o escribir en el servidor.
- View-Model : Código javascript puro.
- Vista : Código HTML con “bindings” declarativos para enlazar el view-model. También se pueden utilizar templates para generar el código html.
Ejemplo Simple
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:
- Twitter Bootstrap (Para el aspecto de la página)
- jQuery
- jQuery tmpl
- knockout.js
<span class="nt"><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">></span>
<span class="nt"><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">></script></span>
<span class="nt"><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">></script></span>
<span class="nt"><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">></script></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.
ViewModel
El viewModel contará con un listado de post que se mostrarán en la tabla
<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.
Y añadimos en binding en la tabla para que renderice la plantilla “postRow” con el listado de “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.
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’.
<span class="nt"><form></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"clearfix"</span><span class="nt">></span>
<span class="nt"><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">/></span>
<span class="nt"></div></span>
<span class="nt"></form></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.
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.
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.
<span class="nt"><form></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"clearfix"</span><span class="nt">></span>
<span class="nt"><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">></span>New post<span class="nt"></a></span>
<span class="nt"><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">></span>Save<span class="nt"></a></span>
<span class="nt"><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">></span>Edit<span class="nt"></a></span>
<span class="nt"><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">></span>Delete<span class="nt"></a></span>
<span class="nt"></div></span>
...</code></pre></figure>
La plantilla muestra el texto o un input dependiendo del campo editMode.
Añadimos las acciones al modelo.
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.