Como ya sabes, hemos cubierto React y Angular.js en anteriores artículos, pero existe una nueva librería frontend que de seguro te interesará. Se llama Vue.js y cuenta con una gran comunidad de entusiastas desarrolladores detrás.
La filosofía de Vue.js es proporcionarnos una API lo más simple posible para crear proyectos de tal manera que la vista (HTML) y el modelo (Javascript) fluyan en perfecta sincronía. Como verás en los siguientes ejemplos, la librería se mantiene fiel a esa idea y es por eso que es muy sencillo trabajar con ella, sin comprometer ninguna otra funcionalidad.
Empezando
La manera más sencilla de instalar Vue.js es simplemente añadiendo el tag script al final del body de tu HTML. Toda la librería está agrupada en un único archivo Javascript que puedes descargarte desde su página oficial o importarla directamente desde CDN:
<script src="http://cdnjs.cloudflare.com/ajax/libs/vue/1.0.16/vue.js"></script>
Si quieres usar la librería en un proyecto Node.js, puedes hacerlo ya que también está disponible como un módulo npm. También cuenta con una CLI oficial, que permite a los usuarios poder desarrollar sus propios proyectos basados en plantillas prefabricadas.
A continuación puedes ver cuatro ejemplos de aplicaciones que hemos desarrollado para ti. Sin más dilación, vamos a explicar Vue.js a través de unos sencillos ejemplos.
Menú de navegación
Para empezar, vamos a hacer una sencilla barra de navegación. Aprovecharemos también para explicar los componentes básicos que toda aplicación de Vue.js debe tener.
- El modelo, o en otras palabras, los datos de la aplicación. En Vue.js es un objecto de Javascript que contiene variables y sus valores iniciales.
- Una plantilla HTML, aunque la correcta terminología es vista. Aquí escogemos qué queremos mostrar, añadir eventos de listerners, y gestionar los diferentes usos del modelo.
- VistaModelo, es una instancia de Vue que une la vista y el modelo, tal y como su propio nombre indica, habilitando grácilmente la comunicación de uno con el otro.
La idea detrás de todas estas explicaciones es que el modelo y la vista siempre deben estar en sincronía. Cambiando el modelo, automáticamente se modificará la vista, y viceversa. En nuestro primer ejemplo te mostramos esto con la variable active, que representa qué elemento del menú está actualmente seleccionado.
HTML
<div id="main">
<!-- The navigation menu will get the value of the "active" variable as a class. -->
<!-- To stops the page from jumping when a link is clicked
we use the "prevent" modifier (short for preventDefault). -->
<nav v-bind:class="active" v-on:click.prevent>
<!-- When a link in the menu is clicked, we call the makeActive method,
defined in the JavaScript Vue instance. It will change the value of "active". -->
<a href="#" class="home" v-on:click="makeActive('home')">Home</a>
<a href="#" class="projects" v-on:click="makeActive('projects')">Projects</a>
<a href="#" class="services" v-on:click="makeActive('services')">Services</a>
<a href="#" class="contact" v-on:click="makeActive('contact')">Contact</a>
</nav>
<!-- The mustache expression will be replaced with the value of "active".
It will automatically update to reflect any changes. -->
<p>You chose <b>{{active}}</b></p>
</div>
JS
// Creating a new Vue instance and pass in an options object. var demo = new Vue({ // A DOM element to mount our view model. el: '#main', // This is the model. // Define properties and give them initial values. data: { active: 'home' }, // Functions we will be using. methods: { makeActive: function(item){ // When a model is changed, the view will be automatically updated. this.active = item; } } });
CSS
*{ margin:0; padding:0; } body{ font:15px/1.3 'Open Sans', sans-serif; color: #5e5b64; text-align:center; } a, a:visited { outline:none; color:#389dc1; } a:hover{ text-decoration:none; } section, footer, header, aside, nav{ display: block; } /*------------------------- The menu --------------------------*/ nav{ display:inline-block; margin:60px auto 45px; background-color:#5597b4; box-shadow:0 1px 1px #ccc; border-radius:2px; } nav a{ display:inline-block; padding: 18px 30px; color:#fff !important; font-weight:bold; font-size:16px; text-decoration:none !important; line-height:1; text-transform: uppercase; background-color:transparent; -webkit-transition:background-color 0.25s; -moz-transition:background-color 0.25s; transition:background-color 0.25s; } nav a:first-child{ border-radius:2px 0 0 2px; } nav a:last-child{ border-radius:0 2px 2px 0; } nav.home .home, nav.projects .projects, nav.services .services, nav.contact .contact{ background-color:#e35885; } p{ font-size:22px; font-weight:bold; color:#7d9098; } p b{ color:#ffffff; display:inline-block; padding:5px 10px; background-color:#c4d7e0; border-radius:2px; text-transform:uppercase; font-size:18px; }
Como puedes ver, trabajar con esta librería es muy sencillo. Vue.js hace todo el trabajo sucio por nosotros y nos proporciona una sintaxis familiar y fácil de recordar.
- Objeto de Javascript para todas las opciones
- {{dobles llaves}} para las plantillas
- v-algo para añadir funciones directamente desde el HTML
Editor online
En el ejemplo anterior, nuestro modelo tenía sólo un par de valores predefinidos. Si queremos dar a los usuarios la capacidad de establecer cualquier dato, podemos unir el campo input con una propiedad del modelo. Cuando introduce un texto, se guarda automáticamente en la variable del modelo TEXT_CONTENT, que a su vez hace que el objeto se actualice.
HTML
<!-- v-cloak hides any un-compiled data bindings until the Vue instance is ready. -->
<!-- When the element is clicked the hideTooltp() method is called. -->
<div id="main" v-cloak v-on:click="hideTooltip" >
<!-- This is the tooltip.
v-on:clock.stop is an event handler for clicks, with a modifier that stops event propagation.
v-if makes sure the tooltip is shown only when the "showtooltip" variable is truthful -->
<div class="tooltip" v-on:click.stop v-if="show_tooltip">
<!-- v-model binds the contents of the text field with the "text_content" model.
Any changes to the text field will automatically update the value, and
all other bindings on the page that depend on it. -->
<input type="text" v-model="text_content" />
</div>
<!-- When the paragraph is clicked, call the "toggleTooltip" method and stop event propagation. -->
<!-- The mustache expression will be replaced with the value of "text_content".
It will automatically update to reflect any changes to that variable. -->
<p v-on:click.stop="toggleTooltip">{{text_content}}</p>
</div>
JS
// Creating a new Vue instance and pass in an options object. var demo = new Vue({ // A DOM element to mount our view model. el: '#main', // Define properties and give them initial values. data: { show_tooltip: false, text_content: 'Edit me.' }, // Functions we will be using. methods: { hideTooltip: function(){ // When a model is changed, the view will be automatically updated. this.show_tooltip = false; }, toggleTooltip: function(){ this.show_tooltip = !this.show_tooltip; } } })
CSS
/* Hide un-compiled mustache bindings until the Vue instance is ready */ [v-cloak] { display: none; } *{ margin:0; padding:0; } body{ font:15px/1.3 'Open Sans', sans-serif; color: #5e5b64; text-align:center; } a, a:visited { outline:none; color:#389dc1; } a:hover{ text-decoration:none; } section, footer, header, aside, nav{ display: block; } /*------------------------- The edit tooltip --------------------------*/ .tooltip{ background-color:#5c9bb7; background-image:-webkit-linear-gradient(top, #5c9bb7, #5392ad); background-image:-moz-linear-gradient(top, #5c9bb7, #5392ad); background-image:linear-gradient(top, #5c9bb7, #5392ad); box-shadow: 0 1px 1px #ccc; border-radius:3px; width: 290px; padding: 10px; position: absolute; left:50%; margin-left:-150px; top: 80px; } .tooltip:after{ /* The tip of the tooltip */ content:''; position:absolute; border:6px solid #5190ac; border-color:#5190ac transparent transparent; width:0; height:0; bottom:-12px; left:50%; margin-left:-6px; } .tooltip input{ border: none; width: 100%; line-height: 34px; border-radius: 3px; box-shadow: 0 2px 6px #bbb inset; text-align: center; font-size: 16px; font-family: inherit; color: #8d9395; font-weight: bold; outline: none; } p{ font-size:22px; font-weight:bold; color:#6d8088; height: 30px; cursor:default; } p b{ color:#ffffff; display:inline-block; padding:5px 10px; background-color:#c4d7e0; border-radius:2px; text-transform:uppercase; font-size:18px; } p:before{ content:'✎'; display:inline-block; margin-right:5px; font-weight:normal; vertical-align: text-bottom; } #main{ height:300px; position:relative; padding-top: 150px; }
Otra cosa a destacar en el código anterior es el atributo v-if. Se mostrará o se ocultará el elemento dependiendo de la veracidad de la variable. Puedes leer más sobre esto aquí.
Formulario de Pedido
Este ejemplo ilustra múltiples servicios y su coste total. Dado que nuestros servicios se almacenan en un array, podemos aprovechar la directiva v-for para recorrer todas las entradas y mostrarlos. Si se añade un nuevo elemento al array o se cambia cualquiera de los anteriores, Vue.js se actualizará automáticamente y mostrará los nuevos datos.
HTML
<!-- v-cloak hides any un-compiled data bindings until the Vue instance is ready. -->
<form id="main" v-cloak>
<h1>Services</h1>
<ul>
<!-- Loop through the services array, assign a click haendler, and set or
remove the "active" css class if needed -->
<li v-for="service in services" v-on:click="toggleActive(service)" v-bind:class="{ 'active': service.active}">
<!-- Display the name and price for every entry in the array .
Vue.js has a built in currency filter for formatting the price -->
{{service.name}} <span>{{service.price | currency}}</span>
</li>
</ul>
<div class="total">
<!-- Calculate the total price of all chosen services. Format it as currency. -->
Total: <span>{{total() | currency}}</span>
</div>
</form>
JS
var demo = new Vue({ el: '#main', data: { // Define the model properties. The view will loop // through the services array and genreate a li // element for every one of its items. services: [ { name: 'Web Development', price: 300, active:true },{ name: 'Design', price: 400, active:false },{ name: 'Integration', price: 250, active:false },{ name: 'Training', price: 220, active:false } ] }, methods: { toggleActive: function(s){ s.active = !s.active; }, total: function(){ var total = 0; this.services.forEach(function(s){ if (s.active){ total+= s.price; } }); return total; } } });
CSS
@import url(https://fonts.googleapis.com/css?family=Cookie); /* Hide un-compiled mustache bindings until the Vue instance is ready */ [v-cloak] { display: none; } *{ margin:0; padding:0; } body{ font:15px/1.3 'Open Sans', sans-serif; color: #5e5b64; text-align:center; } a, a:visited { outline:none; color:#389dc1; } a:hover{ text-decoration:none; } section, footer, header, aside, nav{ display: block; } /*------------------------- The order form --------------------------*/ form{ background-color: #61a1bc; border-radius: 2px; box-shadow: 0 1px 1px #ccc; width: 400px; padding: 35px 60px; margin: 50px auto; } form h1{ color:#fff; font-size:64px; font-family:'Cookie', cursive; font-weight: normal; line-height:1; text-shadow:0 3px 0 rgba(0,0,0,0.1); } form ul{ list-style:none; color:#fff; font-size:20px; font-weight:bold; text-align: left; margin:20px 0 15px; } form ul li{ padding:20px 30px; background-color:#e35885; margin-bottom:8px; box-shadow:0 1px 1px rgba(0,0,0,0.1); cursor:pointer; } form ul li span{ float:right; } form ul li.active{ background-color:#8ec16d; } div.total{ border-top:1px solid rgba(255,255,255,0.5); padding:15px 30px; font-size:20px; font-weight:bold; text-align: left; color:#fff; } div.total span{ float:right; }
Para visualizar los precios en un formato correcto utilizamos uno de los filtros disponibles que vienen con Vue.js. Nos permiten modificar los datos del modelo, para este caso el filtro de la moneda es perfecta, ya que añade el símbolo del euro y los decimalos apropiados. Al igual que los filtros de Angular, se aplican mediante la sintaxis | - {{datos |filtro}}.
Grid Switchable
En nuestro último ejemplo vamos a tratar un escenario común en páginas que tienen distintos layouts. En esta aplicación vamos a mostrar una lista de artículos de programacion.net almacenados en un array.
Al pulsar uno de los botones de la barra superior puedes cambiar entre un diseño grid con imágenes a gran tamaño y un diseño listado con imágenes más pequeñas y texto.
HTML
<form id="main" v-cloak>
<div class="bar">
<!-- These two buttons switch the layout variable,
which causes the correct UL to be shown. -->
<a class="list-icon" v-bind:class="{ 'active': layout == 'list'}" v-on:click="layout = 'list'"></a>
<a class="grid-icon" v-bind:class="{ 'active': layout == 'grid'}" v-on:click="layout = 'grid'"></a>
</div>
<!-- We have two layouts. We choose which one to show depending on the "layout" binding -->
<ul v-if="layout == 'grid'" class="grid">
<!-- A view with big photos and no text -->
<li v-for="a in articles">
<a v-bind:href="a.url" target="_blank"><img v-bind:src="a.image.large" /></a>
</li>
</ul>
<ul v-if="layout == 'list'" class="list">
<!-- A compact view smaller photos and titles -->
<li v-for="a in articles">
<a v-bind:href="a.url" target="_blank"><img v-bind:src="a.image.small" /></a>
<p>{{a.title}}</p>
</li>
</ul>
</form>
JS
var demo = new Vue({ el: '#main', data: { // The layout mode, possible values are "grid" or "list". layout: 'grid', articles: [{ "title": "Cómo crear un módulo en Swift", "url": "http://programacion.net/articulos/como_crear_un_modulo_en_swift_1358", "image": { "large": "http://programacion.net/files/article/20160314050356_swift-big.jpg", "small": "http://programacion.net/files/article/20160314050356_swift.jpg" } }, { "title": "Introducción a Web MIDI", "url": "http://programacion.net/articulos/introduccion_a_web_midi_1357", "image": { "large": "http://programacion.net/files/article/20160313020301_midi-big.jpg", "small": "http://programacion.net/files/article/20160313020301_midi.jpg" } }, { "title": "Gestiona tu FTP desde PHP", "url": "http://programacion.net/articulos/gestiona_tu_ftp_desde_php_1356", "image": { "large": "http://programacion.net/files/article/20160311020354_dirftp-big.jpg", "small": "http://programacion.net/files/article/20160311020354_dirftp.jpg" } }] } });
CSS
/* Hide un-compiled mustache bindings until the Vue instance is ready */ [v-cloak] { display: none; } *{ margin:0; padding:0; } body{ font:15px/1.3 'Open Sans', sans-serif; color: #5e5b64; text-align:center; } a, a:visited { outline:none; color:#389dc1; } a:hover{ text-decoration:none; } section, footer, header, aside, nav{ display: block; } /*------------------------- The search input --------------------------*/ .bar{ background-color:#5c9bb7; background-image:-webkit-linear-gradient(top, #5c9bb7, #5392ad); background-image:-moz-linear-gradient(top, #5c9bb7, #5392ad); background-image:linear-gradient(top, #5c9bb7, #5392ad); box-shadow: 0 1px 1px #ccc; border-radius: 2px; width: 580px; padding: 10px; margin: 45px auto 25px; position:relative; text-align:right; line-height: 1; } .bar a{ background:#4987a1 center center no-repeat; width:32px; height:32px; display:inline-block; text-decoration:none !important; margin-right:5px; border-radius:2px; cursor:pointer; } .bar a.active{ background-color:#c14694; } .bar a.list-icon{ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkYzNkFCQ0ZBMTBCRTExRTM5NDk4RDFEM0E5RkQ1NEZCIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkYzNkFCQ0ZCMTBCRTExRTM5NDk4RDFEM0E5RkQ1NEZCIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RjM2QUJDRjgxMEJFMTFFMzk0OThEMUQzQTlGRDU0RkIiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RjM2QUJDRjkxMEJFMTFFMzk0OThEMUQzQTlGRDU0RkIiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7h1bLqAAAAWUlEQVR42mL8////BwYGBn4GCACxBRlIAIxAA/4jaXoPEkMyjJ+A/g9MDJQBRhYg8RFqMwg8RJIUINYLFDmBUi+ADQAF1n8ofk9yIAy6WPg4GgtDMRYAAgwAdLYwLAoIwPgAAAAASUVORK5CYII=); } .bar a.grid-icon{ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjBEQkMyQzE0MTBCRjExRTNBMDlGRTYyOTlBNDdCN0I4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjBEQkMyQzE1MTBCRjExRTNBMDlGRTYyOTlBNDdCN0I4Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MERCQzJDMTIxMEJGMTFFM0EwOUZFNjI5OUE0N0I3QjgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MERCQzJDMTMxMEJGMTFFM0EwOUZFNjI5OUE0N0I3QjgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4MjPshAAAAXklEQVR42mL4////h/8I8B6IGaCYKHFGEMnAwCDIAAHvgZgRyiZKnImBQsACxB+hNoDAQyQ5osQZIT4gH1DsBZABH6AB8x/JaQzEig++WPiII7Rxio/GwmCIBYAAAwAwVIzMp1R0aQAAAABJRU5ErkJggg==); } .bar input{ background:#fff no-repeat 13px 13px; border: none; width: 100%; line-height: 19px; padding: 11px 0; border-radius: 2px; box-shadow: 0 2px 8px #c4c4c4 inset; text-align: left; font-size: 14px; font-family: inherit; color: #738289; font-weight: bold; outline: none; text-indent: 40px; } /*------------------------- List layout --------------------------*/ ul.list{ list-style: none; width: 500px; margin: 0 auto; text-align: left; } ul.list li{ border-bottom: 1px solid #ddd; padding: 10px; overflow: hidden; } ul.list li img{ width:120px; height:120px; float:left; border:none; } ul.list li p{ margin-left: 135px; font-weight: bold; color:#6e7a7f; } /*------------------------- Grid layout --------------------------*/ ul.grid{ list-style: none; width: 570px; margin: 0 auto; text-align: left; } ul.grid li{ padding: 2px; float:left; } ul.grid li img{ width:280px; height:280px; object-fit: cover; display:block; border:none; }