Ir al contenido principal

This is my blog, more about me at marianoguerra.github.io

🦋 @marianoguerra.org 🐘 @marianoguerra@hachyderm.io 🐦 @warianoguerra

Creemos en la Web: Datos por favor, es promesa

En el capítulo anterior vimos como cargar datos simulando la espera que surge de cargar datos de servicios remotos, ahora vamos a ver como cargar datos remotos de verdad, pero primero tenemos que aprender sobre algo llamado promesas.

En una aplicación web todas las partes del código tienen que colaborar haciendo su trabajo lo mas rápido posible y dejando que otros puedan hacer su trabajo, si un pedazo de código se toma mucho tiempo, otras partes importantes no se ejecutan y se produce algo que te puede haber pasado, que es que la aplicación "se congela".

Es por eso que muchas funcionalidades en js se descomponen en pedazos mas chicos para evitar este problema.

Una de ellas es cargar datos de otros servicios, no podemos darnos el lujo de esperar hasta que respondan, ya que si se toma un par de segundos la aplicación se congela.

La solución es hacer la solicitud y obtener como resultado una promesa.

Una promesa es un objeto que nos permite registrar funciones para cuando la promesa sea cumplida. La promesa puede ser cumplida exitosamente o puede haber un error. También podemos registrar funciones para que corran cuando la promesa sea cumplida, no importa si con éxito o con error.

El tipo de dato promesa (Promise en ingles), no es nada mágico, si no que esta disponible para que la usemos si la necesitamos, vamos a probarla con ejemplos simples, no te preocupes si no entendés la parte de crear promesas, requiere un poco de "pensar de adentro para afuera", normalmente al principio solo "consumimos" promesas, pero es necesario que las creemos así podemos probar todos los casos.

Proba los fragmentos de código en glitch o en la consola en las herramientas de desarrollo.

Una promesa nunca cumplida:

let p1 = new Promise(function (resolve, reject) {});

Esta es la forma mas simple de crear una promesa, el tipo de dato Promise permite crear nuevos objetos de ese tipo con el operador new, el "constructor" de las promesas recibe como argumento una función, la función va a ser llamada en el momento en el que la promesa es construida y dicha función recibe dos argumentos:

resolve

una función a ser llamada si la promesa se cumple con éxito.

reject

una función a ser llamada si la promesa se cumple con un error.

Tanto resolve como reject son funciones que reciben un solo argumento, que va a ser pasado a las funciones que registremos para ambos casos.

Una promesa resuelta instantaneamente con éxito:

let p2 = new Promise(function (resolve, reject) {
    resolve('éxito!');
});

Una promesa resuelta instantaneamente con error:

let p3 = new Promise(function (resolve, reject) {
    reject('error!');
});

Todo muy lindo, pero para que sirve que la resolvamos con éxito o error si no cambia nada?

La utilidad de las promesas es poder registrar una o mas funciones que van a ser llamada en los tres casos que ya mencionamos: éxito, error o cuando se resuelva no importa el caso.

Empecemos registrando una función con la promesa que nunca se resuelve, vamos a usar el método then (entonces en ingles) de los objetos de tipo Promise para registrar una función que va a ser llamada cuando la promesa se cumpla exitosamente, cuando sea llamada va a recibir un argumento que es el valor con el que la promesa se cumplió (el valor prometido ;).

p1.then(function (resultado) {
    alert('p1 resuelta: ' + resultado);
});

A mi en firefox me muestra este resultado, que quiere decir que llamar al método then del objeto promesa devuelve la promesa misma, esto nos va a ser útil después para "encadenar" llamadas a métodos en la promesa.

< Promise { <state>: "pending" }

Fuera de eso, all llamar al método then del objeto promesa p1 no paso nada, es natural ya que esa promesa esta "pendiente" (pending en ingles) y nunca se va a cumplir.

La promesa p2 se cumplió apenas la creamos, que pasa si registramos una función a ser llamada cuando se resuelva con éxito? Probemos:

p2.then(function (resultado) {
    alert('p2 resuelta: ' + resultado);
});

Una aclaración, las funciones que pasamos por parámetro para que sean llamadas en ingles se llaman "callbacks", que traducido es "llamame de vuelta", probablemente use esa palabra de ahora en mas porque es mas corta y para que te acostumbres ya que se usa mucho en la documentación.

Volviendo al código, si lo probaste habrás notado que aparece un cuadro de dialogo mostrando el mensaje "p2 resuelta: éxito!", es decir que si registramos un callback aun después de que la promesa sea cumplida la función va a ser llamada.

Corre el código de nuevo y vas a ver que el dialogo aparece de nuevo, esto es útil ya que no tenemos que preocuparnos si registramos el callback antes o después de que la promesa se resuelva, una cosa a tener en cuenta es que cada callback va a ser llamada una sola vez, ya que cada objeto de promesa puede ser resuelta una vez.

Ahora probemos lo mismo con la promesa que se resuelve con error:

p3.then(function (resultado) {
    alert('p3 resuelta: ' + resultado);
});

El dialogo no aparece... porque then registra callbacks para el caso de éxito, si queremos registrar callbacks para el caso de error, tenemos que usar el método llamado catch (capturar en ingles).

p3.catch(function (resultado) {
    alert('p3 error: ' + resultado);
});

Ahora el dialogo aparece.

Que pasa si queremos hacer algo en ambos casos? hay otro método llamado finally (finalmente en ingles).

p2.finally(function () {
    alert('p2 finally');
});

p3.finally(function () {
    alert('p3 finally');
});

Ambos muestran el dialogo, pero como veras no reciben el valor de resolución porque no sabemos cual sucedió.

Que pasa si queremos hacer un poco de todo, algo si salio bien, por ejemplo actualizar datos, algo si salio mal, por ejemplo mostrar un error y algo siempre, por ejemplo esconder un mensaje de "Cargando".

Obviamente podemos escribir las tres llamadas separadas, pero como mencione las llamadas a los métodos then, catch, finally devuelven la promesa, por lo que podemos hacer algo que se llama "encadenar" llamadas, veamos como es:

p2.then(function (resultado) {
    alert('then: ' + resultado);
})
.catch(function (resultado) {
    alert('catch: ' + resultado);
})
.finally(function () {
    alert('finally');
});

No cambia nada con hacerlo junto o por separado, pero suele hacerse según preferencia así que lo aclaro.

Bueno, basta de promesas (cuac!), veamos como usar esto para cargar datos, para eso vamos a usar una función llamada fetch que hace una solicitud HTTP (como tu navegador para cargar esta pagina, imágenes y datos) y devuelve una promesa, vamos a cargar datos de ejemplo que puse en una pagina:

let url = "https://marianoguerra.github.io/creemos-en-la-web/paginas/promesas/datos.json";
fetch(url);

Ahora con todo lo que sabemos sobre promesas, veamos que nos da la promesa:

fetch(url).then(function (response) {
    console.log(response);
});

Lo que hago es mostrar el valor de response en la consola usando el método log del objeto console (consola en ingles).

A mi en firefox me muestra esto, a vos te puede mostrar algo un poco distinto:

Response { type: "cors", url: "https://marianoguerra.github.io/creemos-en-la-web/paginas/promesas/datos.json", redirected: false, status: 200, ok: true, statusText: "OK", headers: Headers, body: ReadableStream, bodyUsed: false }

Es un objeto de tipo Response (respuesta en ingles) que tiene información variada sobre la solicitud que hicimos, pero lo que nosotros lo que queremos son los datos cuando la solicitud termine, para eso tenemos que pedirle al objeto response que lea el contenido de la respuesta. El objeto response tiene muchos métodos, uno de ellos es el método text, que nos devuelve... otra promesa..., la cual al resolverse nos da el contenido de la solicitud.

fetch(url).then(function (response) {
    response.text().then(function (text) {
        console.log('Texto!', text);
    });
});

Al correrlo debería mostrar lo siguiente en la consola:

Texto! {
    "numero": 42,
    "lista": [1, 2, 3]
}

Como veras el contenido es texto, pero notaras que son datos javascript, el subset de javascript que sirve para describir datos y enviarlos entre computadoras se llama JSON (pronunciado yeison, acrónimo en ingles de JavaScript Object Notation, que significa Notación de Objetos JavaScript).

Hay un objeto llamado JSON que tiene dos métodos útiles, uno llamado parse que recibe como argumento un valor de tipo texto (string) y nos devuelva los datos representados en ese texto, este es el que necesitamos, probemoslo:

fetch(url).then(function (response) {
    response.text().then(function (text) {
        let datos = JSON.parse(text);
        console.log('Datos!', datos);
    });
});

Por suerte como esta es una actividad común, el objeto response tiene un método llamado json que hace la tarea por nosotros:

fetch(url).then(function (response) {
    response.json().then(function (datos) {
        console.log('Datos!', datos);
    });
});

Ya que estamos hablando de JSON veamos el otro método stringify (algo así como "hacer texto" en ingles), que es el inverso de parse, es decir, recibe datos y nos devuelve la representación JSON de esos datos en un valor de tipo texto (string):

JSON.stringify({numero: 42, lista: [1, 2, 3]});

El resultado es:

< "{\"numero\":42,\"lista\":[1,2,3]}"

Para estar seguros de que funciona, probemos el inverso:

JSON.parse("{\"numero\":42,\"lista\":[1,2,3]}");

Notar que para poder insertar comillas dobles en un string, que ya tiene comillas dobles para indicar comienzo y fin, necesitamos poner una barra invertida antes de la comilla, para indicarle que no es el fin del string, sino que queremos poner esa comilla "dentro" del string. Esto normalmente se llama "escapar" caracteres.

Para finalizar, solo recordar que como fetch devuelve una promesa, podemos "encadenar" llamadas a then, catch y finally para hacer distintas operaciones según cual fue el resultado de la solicitud.

La forma general es:

fetch(url).then(function (response) {
    // resultado de la solicitud
})
.catch(function (error) {
    // si hubo error
})
.finally(function (error) {
    // cuando la solicitud termino
});

Resumiendo, aprendimos sobre promesas, llamadas encadenadas, la función fetch para hacer solicitudes a otros servicios, el formato JSON y su objeto con sus métodos parse y stringify.

Creemos en la Web: Datos por favor, pero después

En el capítulo anterior vimos como pedirle datos al usuario, pero en el momento de validar los datos, lo hicimos simple con lógica fija en el código.

En una aplicación real, le preguntaríamos a un servicio que tiene registradas las cuentas si el usuario y la contraseña son validas.

Este tipo de servicios suelen llamarse APIs (interfaz de programación de aplicaciones en ingles).

Las APIs son como funciones pero que corren en otras computadoras, razón por la cual al llamarlas no devuelven instantáneamente si no que demoran un buen rato, así que tenemos que aprender a esperar y manejar distintos casos.

Por ejemplo cuando la solicitud funciona correctamente y cuando falla, ya sea por problemas de red, porque mandamos parámetros inválidos, porque el servicio no esta funcionando correctamente o una gran variedad de causas normalmente resumidas con "se cayo el sistema".

Empecemos cargando datos y mostrandolos, arrancamos con todo el HTML ya que no hay nada nuevo:

<!doctype html>
<html>
  <head>
    <!-- para que muestre bien tildes y caracteres especiales -->
    <meta charset="utf-8">

    <!-- para que se vea bien en Internet Explorer -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- para que se vea bien en telefonos mobiles -->
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Cargando Datos</title>

    <!-- importamos vue para poder crear aplicaciones con javascript https://vuejs.org/ -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
    <!-- importamos las hojas de estilo base bootstrap https://getbootstrap.com/ -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

    <!-- importamos nuestro estilo especifico -->
    <link rel="stylesheet" href="/style.css">

    <!-- importamos nuestro javascript -->
    <script src="/client.js" defer></script>
  </head>
  <body class="m-3">

    <!-- la raíz de nuestra aplicación -->
    <div id="app">
      <div v-if="cargando">
        <div class="alert alert-info">
          Cargando
        </div>
      </div>
      <div v-else="">
        <table class="table table-bordered table-stripped table-hover">
          <tbody>
            <tr><td>Fecha</td><td>{{datos.fecha}}</td></tr>
            <tr><td>Numero</td><td>{{datos.numero}}</td></tr>
            <tr><td>Nombre</td><td>{{datos.nombre}}</td></tr>
          </tbody>
        </table>

        <div class="text-center">
          <div @click="recargar()" class="btn btn-primary">
            Recargar
          </div>
        </div>
      </div>
    </div>

  </body>
</html>

Notaras que hice copy/paste del capitulo anterior, lo único que cambio es lo que esta dentro del div app, pero tampoco es algo nuevo, veamoslo en detalle:

El primer div solo se muestra si el atributo cargando es true, mostrando el mensaje "Cargando".

<div v-if="cargando">
  <div class="alert alert-info">
    Cargando
  </div>
</div>

Sino (con v-else que es como el else del if en javascript) significa que ya cargamos los datos, osea que los podemos mostrar, asumimos que los datos tienen al menos 3 campos: fecha, numero y nombre.

<div v-else="">
  <table class="table table-bordered table-stripped table-hover">
    <tbody>
      <tr><td>Fecha</td><td>{{datos.fecha}}</td></tr>
      <tr><td>Numero</td><td>{{datos.numero}}</td></tr>
      <tr><td>Nombre</td><td>{{datos.nombre}}</td></tr>
    </tbody>
  </table>

Finalmente ponemos un botón que permite recargar los datos.

<div class="text-center">
  <div @click="recargar()" class="btn btn-primary">
    Recargar
  </div>
</div>

La lógica de nuestra aplicación esta en el archivo client.js, empecemos con una base que ya conocemos:

/*globals Vue*/

function main() {
  let app = new Vue({
    el: '#app',
    methods: {
      recargar: function () {
        // cargar aca
      }
    },
    data: {
      cargando: true,
      datos: {
      }
    }
  });

  app.recargar();
}

window.addEventListener('load', main);

El código no hace nada nuevo, y de hecho no esta terminado, pero repasemos.

window.addEventListener('load', main);

Cuando la pagina termine de cargar llamamos a la función main.

La función main crea un nuevo objeto Vue, que va a funcionar en el tag con id "app", la aplicacion tiene dos atributos:

cargando

indica si la aplicación esta cargando datos

datos

los datos cargados, inicialmente vacíos

Nuestra aplicación también tiene un método, recargar, que por ahora no hace nada, osea que si probas esto la pagina se va a quedar en "Cargando" por siempre.

En el método recargar podríamos hacer algo como:

recargar: function () {
  this.datos = {nombre: "bob", numero: 42, fecha: "una fecha"};
  this.cargando = false;
}

Pero eso setearia datos instantáneamente, nunca veríamos el mensaje "Cargando", si solo hubiera una forma de pedirle al navegador que llame a una función "después"...

Obviamente dicha funcionalidad existe y suele ser muy útil, por ejemplo para mostrar o esconder mensajes después de un tiempo prudente.

Esta funcionalidad esta disponible en el objeto window y se llama setTimeout, el método recibe dos argumentos, el primero es la función a llamar, el segundo es cuanto tiene que pasar entre el momento en el que llamamos al método y cuando se va a llamar a nuestra función, en milisegundos (1 segundo son 1000 milisegundos).

recargar: function () {
  let app = this;

  function cargarFalso() {
      app.datos = {nombre: "bob", numero: 42, fecha: "una fecha"};
      app.cargando = false;
  }

  app.cargando = true;

  window.setTimeout(cargarFalso, 3000);
}

Hay un par de detalles importantes en ese código, vamos por partes:

Primero creamos una variable llamada app que tiene el mismo valor que this, porque haríamos eso? lo vamos a ver en un par de lineas.

let app = this;

Luego creamos una función llamada cargarFalso, que se llama así porque va a simular que estamos cargando los datos de un servicio esperando un poco y seteando los datos.

Lo primero para notar es que podemos crear una función dentro de otra función o método, esto lo hacemos acá por dos razones.

La primera razón es porque a esta función solo la necesitamos en el método recargar, así que hacerla "visible" al resto del código es innecesario y puede llegar a hacernos pensar después que es una función que tiene algún tipo de utilidad general cuando en realidad no la tiene fuera del método recargar.

La segunda es que queremos tener acceso a la variable app que esta disponible en los métodos de nuestra aplicación vue, las funciones "ven" las variables declaradas en su "cuerpo" (body en ingles, lo que esta entre llaves), entonces al declararla dentro del método recargar tenemos acceso a la variable sin necesidad de hacer cosas raras, ya que window.setTimeout llama a una función pero no le pasa ningún argumento.

En el cuerpo de la función cargarFalso podemos ver que seteamos el atributo datos de la variable app y aquí revelamos el misterio de la linea let app = this;, como vimos en el capitulo anterior, cada variable tiene un this implícito, su valor depende del "valor antes del punto" al llamarla, osea que cada función puede tener un valor distinto de this o ninguno si llamamos a la función directamente y no como un método.

Para asegurarnos de setear datos en el objeto adecuado, asignamos el valor de this en el método de nuestra aplicación a otra variable para que no se confunda con el this de la función cargarFalso.

No te preocupes, esta es una de las cosas mas confusas de javascript y te puede llevar una buena cantidad de tropiezos terminar de entenderlo por completo.

function cargarFalso() {
    app.datos = {nombre: "bob", numero: 42, fecha: "una fecha"};
    app.cargando = false;
}

Seteamos el atributo cargando de nuestra aplicación a true así la pagina cambia y muestra el mensaje "Cargando" hasta que la función cargarFalso sea llamada.

app.cargando = true;

Finalmente llamamos al método setTimeout del objeto window y le pasamos nuestra función a llamar y cuando la tiene que llamar (en 3000 milisegundos, es decir 3 segundos).

window.setTimeout(cargarFalso, 3000);

Si probas esto vas a ver que la pagina va a cargar, mostrar el mensaje "Cargando" y 3 segundos después va a mostrar los datos.

Si apretamos el botón "Recargar" va a mostrar el mensaje y 3 segundos después los datos.

Hagamos una pequeña modificación para que el cambio se note al hacer click en "Recargar" hagamos que el valor del campo fecha cambie.

Para obtener la fecha y hora actual existe un tipo de objeto llamado "Date" (fecha en ingles).

Cuando creamos un nuevo objeto del tipo Date el objeto que nos devuelve contiene la fecha y hora actual según el reloj de tu computadora, probemos un poco:

Creamos un nuevo objeto de tipo Date usando new:

let date1 = new Date();

Llamamos al método toString que devuelve una representación de la fecha como texto:

date1.toString();
< 'Tue Jan 01 2019 17:41:01 GMT-0300 (-03)'

Llamamos al método toLocaleString que devuelve una representación de la fecha como texto adaptada al idioma y configuración de tu computadora, en tu caso probablemente aparezca distinto al mio, que tengo un lio de configuraciones de ingles, español y alemán:

date1.toLocaleString();
< '2019-1-1 17:41:01'

El método toLocaleDateString devuelve una representación de la fecha, sin la parte del tiempo, como texto adaptada al idioma y configuración de tu computadora:

date1.toLocaleDateString();
< '2019-1-1'

El método toLocaleTimeString devuelve una representación de la fecha, solo la parte del tiempo, como texto adaptada al idioma y configuración de tu computadora:

date1.toLocaleTimeString();
< '17:41:01'

El método getDate Devuelve el día del mes.

No confundirlo con getDay que devuelve el día de la semana.

date1.getDate();
< 1

El método getMonth devuelve el numero de mes, para complicarnos la existencia enero es 0 en lugar de 1:

date1.getMonth();
< 0

El método getFullYear devuelve el año.

No confundirlo con getYear que devuelve el año menos 1900, osea que 2019 es 119`, si, no tiene sentido, como muchas cosas que vamos a aprender.

date1.getFullYear();
< 2019

El método getHours devuelve la hora.

date1.getHours();
< 17

El método getMinutes devuelve los minutos.

date1.getMinutes();
< 41

El método getSeconds devuelve los segundos.

date1.getSeconds();
< 1

Esos son los principales métodos, y luego de este breve tour alternativo volvemos a lo que nos compete, usar new Date() para mostrar la hora del momento en el que cargaron los datos:

Cambiamos cargarFalso para que en el campo fecha tenga la fecha actual en texto:

function cargarFalso() {
    let fecha = new Date(),
        fechaTexto = fecha.toLocaleString();

    app.datos = {nombre: "bob", numero: 42, fecha: fechaTexto};
    app.cargando = false;
}

Ahora al apretar "Recargar" la fecha debería cambiar indicandonos que cargo de nuevo.

Ya aprendimos mucho, como "llamar funciones después", como declarar funciones dentro de otras para tener acceso a las variables del cuerpo de la función, como no confundir el this de distintas funciones creando variables y sobre el tipo de dato Date y alguno de sus métodos, pero lo mejor esta por llegar, en la próxima vamos a usar todo esto que aprendimos para cargar los datos de un servicio/servidor/API.

El resultado esta en https://marianoguerra.github.io/creemos-en-la-web/paginas/datos-despues/2/

Podes explorar el código con las herramientas de desarrollo.

Creemos en la Web: Datos por favor, pero validos

Ya sabemos suficiente HTML, CSS y Javascript para hacer una aplicación completa, así que empecemos y aprendamos lo que falta a medida que lo vayamos necesitando.

Gran parte de las aplicaciones implica mostrar y solicitar datos, así que probemos solicitar datos, mas conocidos como "formularios" o "forms" para los amigos.

El primero que nos vamos a encontrar en cualquier aplicación es el de autenticación, donde pedimos usuario y contraseña y lo validamos, si es valido pasamos a la siguiente pagina, si no, mostramos un mensaje de error.

Empecemos con un "esqueleto de aplicación vue", es decir, los componentes necesarios pero sin que haga algo en particular.

Para eso necesitamos un archivo html, llamemoslo index.html:

<!doctype html>
<html>
  <head>
    <!-- para que muestre bien tildes y caracteres especiales -->
    <meta charset="utf-8">

    <!-- para que se vea bien en Internet Explorer -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <!-- para que se vea bien en telefonos mobiles -->
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Login</title>

    <!-- importamos vue para poder crear aplicaciones con javascript https://vuejs.org/ -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
    <!-- importamos las hojas de estilo base bootstrap https://getbootstrap.com/ -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">

    <!-- importamos nuestro estilo especifico -->
    <link rel="stylesheet" href="/style.css">

    <!-- importamos nuestro javascript -->
    <script src="/script.js" defer></script>
  </head>
  <body class="m-3">

    <!-- la raíz de nuestra aplicación -->
    <div id="app">
      <h1>Soy una aplicación vue.js basica</h1>
    </div>

  </body>
</html>

Si no te acordás algunos de los elementos podes repasarlos de capítulos anteriores o enterarte que yo no los escribí de memoria sino que los fui a buscar a un capitulo anterior y lo copie acá ;)

Para darle "vida" a esta aplicación vamos a necesitar un poco de Javascript, indicado por la presencia del tag <script src="/script.js"></script> en el html.

Ahora creemos un archivo llamado script.js:

/*globals Vue*/

function main(event) {
  let app = new Vue({
    el: '#app',
    data: {}
  });
}

window.addEventListener('load', main);

El código define una función llamada main (principal en ingles) que va a ser el "punto de entrada" de nuestra aplicación.

Pero cuando la llamamos?

Necesitamos llamarla cuando la pagina termine de cargar, para saber eso le podemos decir al objeto window que cuando termine de cargar llame a la función main.

En HTML/Javascript distintos elementos pueden emitir eventos para notificar de cosas que suceden, uno de ellos es la ventana que nos puede avisar cuando termino de cargar.

Para poder reaccionar a estos eventos, tenemos que "escuchar" (listen en ingles).

Los objetos que emiten eventos tienen un método llamado addEventListener (recordemos que un método es una función asociada a un objeto que podemos llamar con objeto.metodo()).

El método addEventListener ("añadir oyente de evento" en ingles) recibe dos argumentos, el primero es el nombre del evento que queremos escuchar, en este caso es el evento 'load'. El segundo argumento es la función a llamar cuando ese evento suceda. La función va a ser llamada con un argumento que contiene información sobre el evento que acaba de suceder, en nuestro caso no nos hace falta.

Cuando la pagina cargue, el objeto window (ventana) va a emitir el evento 'load' y va a buscar todas las funciones que escuchan ese evento y las va a llamar pasandoles un objeto evento con la información del evento.

En ese momento nuestra función main sera llamada, la cual va a crear un nuevo objeto de tipo Vue pasandole opciones de inicialización en un objeto, las opciones de inicialización por ahora son:

el

id del tag donde la aplicación va a correr, en nuestro caso es el id app, para indicarle que es un id le ponemos el # adelante (después vamos a ver otras formas de indicarle el tag raiz.

data

los datos iniciales de nuestra aplicación, por ahora un objeto vacio.

Por si te dio curiosidad que es el /*globals Vue*/, es para indicarle al programa que chequea errores que la variable Vue existe porque otro script la definió, así no nos dice que estamos refiriéndonos a una variable inexistente.

Acá tenes el proyecto base en glitch.com, hace click en Remix to Edit arriba a la derecha para hacerle cambios, hace click en Edit abajo a la derecha para abrir este ejemplo en una pagina completa para editarlo.

Abajo a la izquierda podes cambiar entre ver el código (Code), la aplicación (App) o ambas (Both).

Muy lindo todo pero esta aplicación no hace nada, empecemos por agregar los campos de usuario y contraseña, para eso los definimos en el estado inicial de nuestra aplicación (el campo data):

/*globals Vue*/

function main(event) {
  let app = new Vue({
    el: '#app',
    data: {
        username: '',
        password: ''
    }
  });
}

window.addEventListener('load', main);

Luego, como personas eficientes que somos, vamos a un ejemplo de bootstrap de pagina de login https://getbootstrap.com/docs/4.1/examples/sign-in/ apretamos el botón derecho sobre la pagina y elegimos la opción que dice algo así como "Ver Código Fuente" o "View Page Source" (puede variar en tu navegador), copiamos la parte del HTML que nos interesa, la pegamos en nuestro proyecto y la adaptamos a nuestras necesidades, a mi me quedo así:

<div id="app">
    <h1 class="h3 mb-3 font-weight-normal">Login</h1>

    <label for="inputUser">Usuario</label>
    <input v-model="username" type="text" id="inputUser" class="form-control" required autofocus>

    <label for="inputPassword" class="mt-3">Password</label>
    <input v-model="password" type="password" id="inputPassword" class="form-control" required>

    <button class="btn btn-primary btn-block mt-3" type="submit">Enviar</button>
</div>

Usando el atributo v-model en los tags input conectamos los valores de nuestra aplicación al HTML.

Ahora necesitamos que cuando se haga click en el botón comprobemos los valores, para eso vamos a usar el atributo @click en el tag button:

<button @click="login()" class="btn btn-primary btn-block mt-3" type="submit">Enviar</button>

Pero a que función o método login llama? al método login de nuestra aplicación, razón por la cual tenemos que definirlo en el atributo methods:

/*globals Vue*/

function main(event) {
  let app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        if (this.username === 'bob' && this.password === 'secreto') {
          alert("Éxito");
        } else {
          alert("Error");
        }
      }
    },
    data: {
      username: '',
      password: ''
    }
  });
}

window.addEventListener('load', main);

Lo que login hace por ahora es comprobar si el usuario es 'bob' y la contraseña es 'secreto', si es así muestra un dialogo con el mensaje "Éxito", sino muestra un dialogo con el mensaje "Error".

Fijate que para acceder a los campos username y password dentro del método login de nuestro objeto app, usamos this.username y this.password.

La variable this es una variable que esta disponible en métodos (osea, funciones de objetos), y su valor es el objeto al que el método pertenece, this significa "este" en ingles, por lo que podemos acceder a los atributos y otros métodos del objeto actual usando this.

Dicho de otra forma, si tenemos dos aplicaciones vue app1 y app2 y llamamos a login en ambas, this se va a referir a la aplicación correcta.

app1.login(); // this es app1
app2.login(); // this es app2

Dicho de otra otra forma, this es el valor a la izquierda del punto cuando llamamos a un método.

Proba login con distintos usuarios y contraseñas.

Lo que podemos hacer ahora es, si el usuario y contraseña son correctos, ir a otra pagina.

Para eso necesitamos saber en que pagina estamos, si en login o la pagina principal y según eso mostrar una pagina o la otra. Para eso vamos a agregar un atributo mas a nuestra aplicación, llamado view (vista e ingles), si view es 'login', mostramos lo que veníamos mostrando hasta ahora, si view es 'main' (principal en ingles), mostramos una pagina donde le damos la bienvenida.

Ya que estamos, vamos a mostrar un mensaje mas agradable si el login es invalido, para eso vamos a agregar otro atributo, llamado error, que si no es vacio va a mostrar un mensaje de error en la pantalla:

function main(event) {
  let app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        if (this.username === 'bob' && this.password === 'secreto') {
          this.view = 'main';
        } else {
          this.error = 'Usuario o contraseña incorrecta';
        }
      }
    },
    data: {
      view: 'login',
      error: '',
      username: '',
      password: ''
    }
  });
}

El HTML queda así:

<div id="app">

  <div v-if="view === 'login'">
    <h1 class="h3 mb-3 font-weight-normal">Login</h1>
    <label for="inputUser">Usuario</label>
    <input v-model="username" type="text" id="inputUser" class="form-control" required autofocus>
    <label for="inputPassword" class="mt-3">Password</label>
    <input v-model="password" type="password" id="inputPassword" class="form-control" required>

    <div v-if="error !== ''" class="alert alert-danger mt-3">
      {{error}}
    </div>

    <button @click="login()" class="btn btn-primary btn-block mt-3">Enviar</button>
  </div>

  <div v-if="view === 'main'">
    <h1>Hola {{username}}!</h1>
  </div>

</div>

Fijate que la estructura base es:

<div id="app">

  <div v-if="view === 'login'">
    <!-- pagina de login acá -->
  </div>

  <div v-if="view === 'main'">
    <!-- pagina principal acá -->
  </div>

</div>

Usamos v-if para incluir o no distintos tags (y sus descendientes) según el valor del atributo view, lo mismo hacemos con el mensaje de error:

<div v-if="error !== ''" class="alert alert-danger mt-3">
  {{error}}
</div>

Para cerrar el ciclo, vamos a permitir volver a la pagina de login, osea que vamos a agregar un botón "logout" que permita ir de la pagina principal al login:

Cambiamos la sección principal para que quede:

<div v-if="view === 'main'">
    <h1>Hola {{username}}!</h1>
    <a href="#" @click="logout()" class="float-right">logout</a>
</div>

Y agregamos el método logout a la lista de métodos de la aplicación, donde seteamos view a 'login' y por las dudas limpiamos el mensaje de error, usuario y password.

function main(event) {
  let app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        if (this.username === 'bob' && this.password === 'secreto') {
          this.view = 'main';
        } else {
          this.error = 'Usuario o contraseña incorrecta';
        }
      },
      logout: function () {
        this.view = 'login';
        this.error = '';
        this.username = '';
        this.password = '';
      }
    },
    data: {
      view: 'login',
      error: '',
      username: '',
      password: ''
    }
  });
}

El resultado final para explorarlo:

Creemos en la Web: No nos llames, nosotros te llamamos

En el capítulo anterior vimos como acceder a elementos y atributos individuales de una colección o a todos ellos iterando con el ciclo for.

en este capítulo vamos a ver como usar funciones para evitar repetir código y evitar errores comunes.

Las funciones también son valores

Hasta ahora declaramos funciones con la palabra clave function y las llamamos usando su nombre, paréntesis y una lista de argumentos.

Pero quizás notaste que el nombre de las funciones es un identificador como cualquier otra variable, que pasa si intentamos usarla como cualquier otra variable?

Probemos con una función simple:

function incrementar(valor) {
    return valor + 1;
}

Probemosla:

incrementar(10);
< 11

Que pasa si solo la nombramos?:

incrementar;
< [Function: incrementar]

nada muy útil, pero nos indica que las funciones son valores como cualquier otra cosa y como cualquier otra cosa en javascript podemos pasarla como parámetro a funciones y devolverla de funciones.

Probemos pasarla como parámetro a una función, pero, para que la usaríamos? lo único que sabemos hacer con una función es llamarla, así que hagamos eso:

function llamar(fn, valor) {
    return fn(valor);
}

Probemos:

llamar(incrementar, 25);
< 26

Funciona, pero no es muy útil que digamos, que mas podríamos hacer?

Como mencione antes, una buena practica en programación es no repetirse mucho, porque al repetirse podemos introducir errores en algunas partes, también hace mas difícil hacer cambios ya que tenemos el mismo código repetido en muchos lugares. También es útil escribir código complicado una sola vez en un solo lugar para evitar errores y hacer mas simple el resto del código.

No se vos, pero escribir todo un for cada vez es un poco molesto, repetitivo y si en lugar de < por error escribimos <= tenemos un error que va a ser difícil de detectar, probemos escribir una función que nos evite escribir el for cada vez:

function porCadaElemento(lista, fn) {
    for (let i = 0, len = lista.length; i < len; i += 1) {
        let elemento = lista[i];
        fn(elemento, i);
    }
}

Lo que la función porCadaElemento hace es recibir una lista y una función como parámetros y llamar a la función porCadaElemento de la lista pasandole dos parámetros, el elemento y el indice del elemento en la lista.

Para probarlo vamos a necesitar una función, empecemos imprimiendo los elementos de la lista:

function imprimirElemento(elemento, i) {
    console.log('elemento', i, 'de lista es', elemento);
}

Ahora llamemos a porCadaElemento con una lista y esta función:

porCadaElemento([10, 20, 30], imprimirElemento);

El resultado es:

elemento 0 de lista es 10
elemento 1 de lista es 20
elemento 2 de lista es 30

Funciona, pero que pasa si queremos hacer algo en cada elemento y acumularlo? creemos una función para eso:

function aCadaElemento(lista, fn) {
    let resultado = [];

    for (let i = 0, len = lista.length; i < len; i += 1) {
        let elemento = lista[i],
            nuevoElemento = fn(elemento, i);

        resultado.push(nuevoElemento);
    }

    return resultado;
}

aCadaElemento se le aplica la función que pasamos por parámetro y el resultado que devuelva se agrega a una nueva lista que devolvemos cuando llamamos a la función para todos los elementos de la lista de entrada, probemos un caso para entenderlo mejor:

aCadaElemento([10, 20, 30], incrementar);
< [ 11, 21, 31 ]

A cada elemento de la lista [10, 20, 30] le "aplicamos" la función incrementar y acumulamos el resultado en una nueva lista que devolvemos.

Otra cosa que podemos hacer es "filtrar" elementos de una lista, con una función que recibe una lista y una función que le indica si cada elemento debe ser descartado de la lista resultado o no.

function filtrar(lista, fn) {
    let resultado = [];

    for (let i = 0, len = lista.length; i < len; i += 1) {
            let elemento = lista[i],
                incluir = fn(elemento, i);

            if (incluir) {
                resultado.push(elemento);
            }
    }

    return resultado;
}

Para probar esta función vamos a necesitar una función nueva que reciba un valor y devuelva true si la condición es verdadera y false si es falsa. Este tipo de funciones se suelen llamar "predicados".

Creemos una que nos diga si un numero es par:

function esPar(n) {
    // si el resto de la división es cero, entonces el numero es par
    return n % 2 === 0;
}

Probemos nuestro "predicado" esPar:

esPar(0);
< true
esPar(1);
< false
esPar(2);
< true
aCadaElemento([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], esPar);
< [ true, false, true, false, true, false, true, false, true, false ]

Ahora probemosla con nuestra función filtrar:

filtrar([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], esPar);
< [ 0, 2, 4, 6, 8 ]

Funciona y la intención del código queda mas clara que si tuviéramos todo el for mezclado con la lógica de lo que queremos hacer.

Una cosa que podemos hacer si la lógica que queremos aplicar es solo útil en esa llamada es no declarar la función de antemano sino declararla directamente en el lugar donde la llamamos, así queda todo junto y no se nos llena el código de funciones que se usan una sola vez, como podemos pasar parámetros de valores directamente sin declarar variables, podemos pasar funciones directamente sin declararlas de antemano:

filtrar([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], function (elemento, i) {
    return elemento % 2 === 0;
});
< [ 0, 2, 4, 6, 8 ]

Estas funciones sin declarar se llaman "funciones anónimas" o "lambda" ya que no tienen nombre, pero es buena practica aun cuando nunca la vamos a llamar por nombre, de darle un nombre para poder entender mas fácil que es lo que hace:

filtrar([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], function esPar(elemento, i) {
    return elemento % 2 === 0;
});
< [ 0, 2, 4, 6, 8 ]

Pero si estas cosas son tan comunes, seguro alguien ya las hizo por nosotros no?

Así es, estas funciones son tan comunes que tienen nombres estándar y vienen incluidas en javascript.

  • porCadaElemento se llama forEach ("porCada" en ingles)

  • aCadaElemento se llama map ("mapear" o "aplicar" en ingles)

  • filtrar se llama filter ("filtrar" en ingles)

Estas funciones están disponibles como atributos en las listas, un atributo que es una función suele llamarse método.

Probemos con los métodos de las listas lo que ya hicimos:

let lista = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
lista.forEach(imprimirElemento);

Imprime:

elemento 0 de lista es 0
elemento 1 de lista es 1
elemento 2 de lista es 2
elemento 3 de lista es 3
elemento 4 de lista es 4
elemento 5 de lista es 5
elemento 6 de lista es 6
elemento 7 de lista es 7
elemento 8 de lista es 8
elemento 9 de lista es 9
lista.map(incrementar);
< [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
lista.filter(esPar);
< [ 0, 2, 4, 6, 8 ]

Este capitulo es uno de los mas densos de todos hasta ahora, pero con lo aprendido hasta acá ya podes programar cualquier cosa :) solo queda un concepto importante por aprender pero no es necesario, sino que es útil para evitar que nuestro código no se vuelva muy complicado.

Creemos en la Web: Uno, dos, muchos

En capítulos anteriores vimos tipos de datos "compuestos", también llamados colecciones, como listas y objetos, también vimos, solo por necesidad, como mostrar elementos de una lista.

En este capitulo vamos a ver en detalle como hacer algo con cada elemento de una lista o un objeto.

Repasando

Recordemos un poco los tipos de datos compuestos/colecciones, escribí cada linea en la consola del navegador (F12 y selecciona el tab Consola o similar) para ver el resultado.

Lista (o Array)

Una lista es una secuencia de cero o mas elementos de cualquier tipo, cada elemento se accede por su posición en la lista, empezando desde la posición cero.

Una lista vacía (sin elementos):

let listaVacia = [];

Una lista con un elemento:

let lista1 = [42];

Una lista con números:

let listaNumeros = [1, 2, 42, 100, 9001];

Una lista con valores de distinto tipo:

let listaTipos = [42, null, undefined, true, "hola", []];

Objeto

Un objeto es un conjunto de cero o mas elementos de cualquier tipo, cada elemento se accede por su clave, que es el nombre que le damos a ese campo, también llamado atributo del objeto.

Un objeto vacío:

let objetoVacio = {};

Un objeto con un campo:

let objetoCampo = {nombre: 'bob'};
// es lo mismo que lo siguiente, siempre y cuando la clave (key)
// sea un identificador valido, sino siempre hay que usar comillas
let objetoCampo1 = {'nombre': 'bob'};

Un objeto con valores de distinto tipo:

let objetoTipos = {numero: 42, null: null, undefined: undefined, booleano: true, texto: "hola", lista: [10, 20, 30], objeto: {clave: 'valor'}, 'clave rara!': 12.5, 100: 'cien'};

Vale aclarar que las claves de los objetos son siempre de tipo texto (strings), por mas que no le pongamos comillas, javascript las va a convertir en texto, por lo que las claves 'null', 'undefined' o si ponemos un numero, no son de ese tipo sino que se convierten en texto antes de guardarlas.

Accediendo a los valores a mano

Como accedemos a los valores almacenados en colecciones? hay dos formas dependiendo la colección, que queremos acceder dentro de esa colección y la clave.

Accediendo elementos de una lista

Como ya dijimos, las listas son una secuencia de elementos de cualquier tipo, donde cada elemento se accede por su posición en la lista, empezando desde la posición cero, probemos con las variables que declaramos arriba, si no las escribiste en la consola, este es un buen momento para hacerlo.

Si ya las escribiste y las intentas declarar de nuevo, puede que obtengas un error de que la variable ya fue declarada, proba con otro nombre o cerra y abrí el tab para poder declararla de nuevo.

Siempre me gusta empezar tratando de romper todo, así que veamos que pasa cuando intentamos acceder a un elemento que no esta definido, por ejemplo, el primer elemento de la lista vacía, que seria el elemento con indice 0, para acceder a un elemento de una colección usamos los corchetes después del nombre de la colección y entre corchetes ponemos el identificador del valor que queremos acceder:

listaVacia[0];

El resultado me da:

< undefined

Es decir que cuando le pedimos un elemento que no tiene nos devuelve undefined.

Pero como sabemos cuantos elementos tiene para saber hasta donde le podemos pedir?

En Javascript todos los valores son "objetos", es decir que tienen atributos y funciones asociadas para operar sobre sus valores, esto quiere decir que le podemos preguntar a una lista cuantos elementos tiene accediendo a su atributo length (que es "longitud" o "largo" en ingles).

Probemos:

listaVacia.length;
< 0

Nos dice que tiene 0 elementos, osea que es una lista vacía.

Como habrás visto usamos otra forma de acceder a un atributo de la lista, esta vez con un punto, esta forma se puede usar cuando el valor del atributo es un identificador valido, esto no quita que podamos hacer:

listaVacia['length'];
< 0

Lo que no es muy útil si el atributo al que queremos acceder es "fijo", pero nos va a ser útil para cuando queramos acceder a atributos que no conocemos cuando estamos escribiendo el programa, para darte una idea de lo que seria, imaginemos que tenemos una función que recibe dos parámetros, un valor y un nombre de un atributo de ese valor y devuelve el valor de ese atributo:

function devolverValor(valor, atributo) {
    return valor[atributo];
}

La función no sabe cual es el valor de atributo, ya que es una variable, puede ser cualquier cosa, probemos con listaVacia y 'length':

devolverValor(listaVacia, 'length');
< 0

Probemos con un indice de la lista:

devolverValor(listaVacia, 0);
< undefined

Probemos con lista1:

devolverValor(lista1, 'length');
< 1
devolverValor(lista1, 0);
< 42

Accediendo elementos de una objeto

Buenas noticias, ya sabes como acceder a elementos de un objeto, las formas son las mismas, con corchetes si la clave no es un identificador valido y con punto si lo es, empecemos rompiendo, tratemos de acceder a un atributo que no esta definido en el objeto:

objetoVacio.yoNoExisto;
< undefined

Como la lista, si no esta definido devuelve undefined. Ahora probemos con uno que si exista:

objetoCampo.nombre;
< 'bob'

Como ya vimos, se puede acceder con corchetes, cosa que no es necesaria en este caso pero probemos:

objetoCampo['nombre'];
< 'bob'

En donde si la vamos a necesitar es con nuestra 'clave rara!', que no podemos acceder con punto ya que no es un identificador valido:

objetoTipos['clave rara!'];
< 12.5

Como habíamos dicho, las claves de los objetos son siempre de tipo texto (string), probemos que es cierto:

objetoTipos[100];
< 'cien'

Eso no demuestra nada... pero esto si:

objetoTipos['100'];
< 'cien'

Como veras también funciona, porque si le pasamos el numero, javascript lo convierte en string, si le pasamos un string lo usa directamente, por eso ambas funcionan.

Iterando (Listas)

Hasta acá podemos acceder elementos de una colección de a uno, sabiendo la clave o indice que queremos acceder de antemano o cuando tenemos la clave/indice en una variable.

Pero muchas veces necesitamos acceder a todos los elementos/atributos de una colección sin saber de antemano cuales son.

Para esto vamos a aprender a iterar sobre los elementos de una colección, hay varias formas de hacerlo, vamos a empezar con una de las mas flexibles y estándares, aunque un poco complicada, así que no te preocupes si no la entendés la primera vez, yo programe por un año antes de entender bien como funcionaba.

Antes de mostrar la sintaxis pensemos que es lo que queremos hacer, para el caso de una lista:

Inicialización:

  • Preguntarle a la lista cuantos elementos tiene y guardarlo en una variable, llamemosle len

  • Declarar una variable a cero, llamemosle i (de indice)

Ciclo:

  • Si el indice es menor que el largo

    • Obtener el valor de la lista en ese indice

    • Hacer algo

    • Incrementar la variable "indice"

    • Ejecutar Ciclo de nuevo

Como veras, la lógica tiene dos partes, una que se ejecuta una sola vez al principio (Inicialización) y una que se ejecuta por cada elemento de la lista, hasta que una condición deje de cumplirse (que el indice este dentro del largo de la lista).

Esto se suele llamar "ciclo for", veamos la sintaxis a grandes rasgos, no es la sintaxis valida pero para darte una idea:

for (inicializarVariables (1); condicionDeCorte (2); siguiente (3)) {
    // lógica por cada paso (4)
}

Veamoslo con código valido, no hace falta que lo entiendas entero, es para que te des una idea:

for (let i = 0, len = lista.length; i < len; i += 1) {
    let elemento = lista[i];
    console.log('elemento', i, 'de lista es ', elemento);
}

Inicialización:

la sección inicializarVariables (1) se ejecuta una sola vez al principio.

Ciclo:

Luego se evalúa la condicionDeCorte (2) para ver si la lógica (4) se va a ejecutar o no.

Si la condición de corte evalúa a false el ciclo termina, si evalúa a true se ejecuta la lógica (4) con el valor de i actual. Luego se evalúa siguiente (3), que normalmente incrementa el valor de i, pero puede hacer otras cosas.

Luego de evaluar siguiente (3), se repite el ciclo.

Probemoslo con varias listas de largos distintos, para eso pongamos nuestro ciclo for en una función:

function mostrarElementos(lista) {
    console.log('antes del ciclo for para', lista);

    for (let i = 0, len = lista.length; i < len; i += 1) {
        let elemento = lista[i];
        console.log('elemento', i, 'de lista es ', elemento);
    }

    console.log('después del ciclo for');
}

Probemoslo con una lista vacía:

mostrarElementos(listaVacia);

Imprime lo siguiente:

antes del ciclo for para []
después del ciclo for

Como podemos ver, el paso (4) nunca se ejecuto, porque la condición de corte (2) evaluó a false la primera vez.

La secuencia completa para listaVacia es:

// Inicialización
// (1)
i = 0
len = 0

// Ciclo
// (2)
i < len // false

// Fin

Probemos con una lista de un elemento:

mostrarElementos(lista1);

Imprime:

antes del ciclo for para [ 42 ]
elemento 0 de lista es  42
después del ciclo for

Ahora el paso (4) y (3) se ejecutaron 1 vez, veamos la secuencia:

// Inicialización
// (1)
i = 0
len = 1

// Ciclo
// (2)
i < len // true

// (4)
elemento = lista[0]; // 42

// (3)
i += 1 // 1

// (2)
i < len // false

// Fin

Ultima, con dos elementos:

mostrarElementos([10, 20]);

Imprime:

antes del ciclo for para [ 10, 20 ]
elemento 0 de lista es  10
elemento 1 de lista es  20
después del ciclo for
// Inicialización
// (1)
i = 0
len = 2

// Ciclo
// (2)
i < len // true

// (4)
elemento = lista[0]; // 10

// (3)
i += 1 // 1

// Ciclo
// (2)
i < len // true

// (4)
elemento = lista[1]; // 20

// (3)
i += 1 // 2

// (2)
i < len // false

// Fin

Iterando (Objetos)

Iterar listas es lo mas común, pero algunas veces necesitamos iterar por todas las claves de un objeto, para eso usamos una versión distinta (y por suerte mas simple) del ciclo for, veamoslo directamente en una función:

function mostrarElementosObjeto(objeto) {
    console.log('antes del ciclo for para', objeto);

    for (let key in objeto) {
        let elemento = objeto[key];
        console.log('elemento', key, 'de objeto es ', elemento);
    }

    console.log('después del ciclo for');
}

Esta versión es mas simple, asigna a key cada clave en objeto y ejecuta la lógica.

Probemos con un objeto vacío:

mostrarElementosObjeto({});

Imprime:

antes del ciclo for para {}
después del ciclo for

Con varios elementos:

mostrarElementosObjeto(objetoTipos);

Imprime:

antes del ciclo for para { '100': 'cien',
  numero: 42,
  null: null,
  undefined: undefined,
  booleano: true,
  texto: 'hola',
  lista: [ 10, 20, 30 ],
  objeto: { clave: 'valor' },
  'clave rara!': 12.5 }

elemento 100 de objeto es  cien
elemento numero de objeto es  42
elemento null de objeto es  null
elemento undefined de objeto es  undefined
elemento booleano de objeto es  true
elemento texto de objeto es  hola
elemento lista de objeto es  [ 10, 20, 30 ]
elemento objeto de objeto es  { clave: 'valor' }
elemento clave rara! de objeto es  12.5

después del ciclo for

Para saber cuantos atributos tiene un objeto, podemos usar una función llamada Object.keys que nos devuelve una lista de las claves de un objeto, probemosla:

Object.keys(objetoVacio);
< []
Object.keys(objetoCampo);
< [ 'nombre' ]
Object.keys(objetoTipos);
< [ '100',
  'numero',
  'null',
  'undefined',
  'booleano',
  'texto',
  'lista',
  'objeto',
  'clave rara!' ]

Veamos como podríamos iterar sobre las claves de un objeto usando Object.keys y lo que aprendimos sobre listas:

function mostrarElementosObjeto1(objeto) {
    console.log('antes del ciclo for para', objeto);

    let keys = Object.keys(objeto);
    for (let i = 0, len = keys.length; i < len; i += 1) {
        let key = keys[i],
            elemento = objeto[key];
        console.log('elemento', key, 'de objeto es ', elemento);
    }

    console.log('después del ciclo for');
}

Probemoslo:

mostrarElementosObjeto1({});

Imprime:

antes del ciclo for para {}
después del ciclo for

Con varios elementos:

mostrarElementosObjeto1(objetoTipos);

Imprime:

antes del ciclo for para { '100': 'cien',
  numero: 42,
  null: null,
  undefined: undefined,
  booleano: true,
  texto: 'hola',
  lista: [ 10, 20, 30 ],
  objeto: { clave: 'valor' },
  'clave rara!': 12.5 }

elemento 100 de objeto es  cien
elemento numero de objeto es  42
elemento null de objeto es  null
elemento undefined de objeto es  undefined
elemento booleano de objeto es  true
elemento texto de objeto es  hola
elemento lista de objeto es  [ 10, 20, 30 ]
elemento objeto de objeto es  { clave: 'valor' }
elemento clave rara! de objeto es  12.5

después del ciclo for

Como podemos ver, funciona igual que mostrarElementosObjeto.

Creemos en la Web: Ver o no ver, también es una cuestión

En el capítulo anterior vimos como calcular o no según el resultado de una condición, ahora vamos a ver como cambiar que mostramos en HTML según el valor de condiciones.

Vamos a hacerlo dándole una cara a nuestro despertador.

Empecemos con la versión mas simple, inicializando un despertador con una sola alarma y mostrando si esta prendido o no y a que hora esta configurada la alarma.

<div id="despertador-app-1">
  <p class="alert alert-info">Despertador Prendido? <strong>{{despertadorPrendido}}</strong></p>
  <p class="alert alert-info">Hora Despertador: {{horaDespertador}}</p>
</div>
<script>
  new Vue({
    el: '#despertador-app-1',
    data: {
      despertadorPrendido: false,
      horaDespertador: 8
    }
  })
</script>

El resultado:

Despertador Prendido? {{despertadorPrendido}}

Hora Despertador: {{horaDespertador}}

Repasando un poco vue.js, tenemos un div raíz de nuestra aplicación, el cual tiene que tener un atributo id así le podemos indicar a vue.

Dentro del div tenemos dos párrafos, el primero muestra el valor de la variable despertadorPrendido usando el formato de vue {{variable}} para indicar que queremos que ahí ponga el valor de variable, el segundo muestra el valor de la variable horaDespertador.

Luego en un tag script inicializamos nuestra aplicación con new Vue(...) donde en ... indicamos usando un objeto javascript dos atributos:

el

Id del tag raíz de esta aplicación, para indicarle que es un id tenemos que prefijarlo con un #.

data

Los datos de nuestra aplicación, por ahora las dos variables que usamos en el HTML arriba y que usamos en el capitulo anterior.

Con esto ya mostramos el estado actual de nuestro simple despertador, pero no podemos prenderlo ni cambiar la hora, veamos como hacer eso:

<div id="despertador-app-2">
  <p v-if ="despertadorPrendido" class="alert alert-success">Despertador Prendido</p>
  <p v-else class="alert alert-danger">Despertador Apagado</p>

  <p class="alert alert-info">Hora Despertador: {{horaDespertador}}</p>

  <input v-model.number="horaDespertador" type="number" class="form-control" placeholder="Hora Despertador">

  <div class="mt-3 text-center">
    <button v-if="despertadorPrendido" v-on:click="despertadorPrendido = false" class="btn btn-danger">Apagar</button>
    <button v-else v-on:click="despertadorPrendido = true" class="btn btn-success">Prender</button>
  </div>
</div>

<script>
  new Vue({
    el: '#despertador-app-2',
    data: {
      despertadorPrendido: false,
      horaDespertador: 8
    }
  })
</script>

El resultado, proba cambiando la hora y haciendo click en el botón:

Despertador Prendido

Despertador Apagado

Hora Despertador: {{horaDespertador}}

Algunos cambios con respecto a la versión anterior, notaras que vue tiene su propio if como javascript, en este caso se escribe v-if="condicion" lo usamos para decidir que párrafo mostramos, si despertadorPrendido es true, mostramos:

<p v-if="despertadorPrendido" class="alert alert-success">Despertador Prendido</p>

Sino:

<p v-else class="alert alert-danger">Despertador Apagado</p>

v-else tiene que estar después de un tag con un atributo v-if o v-else-if.

A la hora la seguimos mostrando de la misma forma pero ahora tenemos un campo de texto para poder cambiarlo:

<input v-model.number="horaDespertador" type="number" class="form-control" placeholder="Hora Despertador">

Con v-model le indicamos a vue que queremos que el contenido de este input refleje el valor del campo horaDespertador, es decir que va a mostrar su valor y cuando lo cambiemos va a actualizar su valor.

Como el campo es un numero y no queremos tener un valor de tipo texto con un numero dentro, le indicamos a vue que nos lo convierta a numero con v-model.number.

El resto es HTML estándar.

Finalmente usamos v-if y v-else de nuevo para mostrar el botón Prender o Apagar según el valor de despertadorPrendido:

<button v-if="despertadorPrendido"
    v-on:click="despertadorPrendido = false"
    class="btn btn-danger">Apagar</button>

<button v-else
    v-on:click="despertadorPrendido = true"
    class="btn btn-success">Prender</button>

En cada botón le indicamos a vue con v-on:click que cuando el botón sea clickeado queremos cambiar el valor de despertadorPrendido.

Con esto ya tenemos una aplicación para prender/apagar un despertador y cambiar la hora de la alarma, pero en el capítulo anterior teníamos una alarma con día, veamos como replicar eso en HTML.

Lo primero que vamos a tener que hacer es tener por cada día de la semana un campo para despertadorPrendido y uno para horaDespertador.

Podemos elegir mostrar todos los días o podemos hacerlo mas simple con un selector del día que queremos ver y manipular.

El resto debería ser como hasta ahora.

Veamos como seria eso:

<div id="despertador-app-3">
  <p v-if="dias[diaSeleccionado].despertadorPrendido"
     class="alert alert-success">Despertador Prendido</p>
  <p v-else
     class="alert alert-danger">Despertador Apagado</p>

  <p class="alert alert-info">Hora Despertador:
    {{dias[diaSeleccionado].horaDespertador}}</p>

  <input v-model.number="dias[diaSeleccionado].horaDespertador"
     type="number" class="form-control" placeholder="Hora Despertador">

  <select v-model="diaSeleccionado" class="custom-select mt-3">
    <option value="lunes">Lunes</option>
    <option value="martes">Martes</option>
    <option value="miercoles">Miércoles</option>
    <option value="jueves">Jueves</option>
    <option value="viernes">Viernes</option>
    <option value="sabado">Sábado</option>
    <option value="domingo">Domingo</option>
  </select>

  <div class="mt-3 text-center">
    <button v-if="despertadorPrendido"
        v-on:click="despertadorPrendido = false"
        class="btn btn-danger">Apagar</button>

    <button v-else
        v-on:click="despertadorPrendido = true"
        class="btn btn-success">Prender</button>
  </div>
</div>

<script>
  new Vue({
    el: '#despertador-app-3',
    data: {
      diaSeleccionado: 'lunes',
      dias: {
        lunes: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        martes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        miercoles: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        jueves: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        viernes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        sabado: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        domingo: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
      }
    }
  })
</script>

Resultado:

Despertador Prendido

Despertador Apagado

Hora Despertador: {{dias[diaSeleccionado].horaDespertador}}

El HTML es bastante similar, solo que donde antes teníamos despertadorPrendido ahora tenemos dias[diaSeleccionado].despertadorPrendido y donde antes teníamos horaDespertador ahora tenemos dias[diaSeleccionado].horaDespertador, ya que tenemos que mostrar y cambiar los valores del día seleccionado.

También tenemos un tag select con v-model seteado a diaSeleccionado así muestra el día seleccionado y si elegimos otro valor en el select el valor de diaSeleccionado es actualizado.

Si no te gusta repetir mucho las cosas como yo, notaras que dias[diaSeleccionado] esta por todos lados, es molesto escribirlo, podemos cometer un error si lo escribimos mal y si llegamos a renombrar algo vamos a tener que ir a todos los lugares a actualizarlo.

Seria mas fácil si pudiéramos "nombrar" este pedazo de código a algo mas claro y simple.

Otra cosa que podrás haber notado si tenes una tendencia a intentar romper todo lo que te dan es que se puede poner horas incoherentes, por ejemplo 42 en el campo de horaDespertador, estaría bueno poder evitar eso.

Veamos como podemos solucionar estos problemas:

<div id="despertador-app-4">
  <p v-if ="diaActual.despertadorPrendido"
     class="alert alert-success">Despertador Prendido</p>
  <p v-else
     class="alert alert-danger">Despertador Apagado</p>

  <p class="alert alert-info">Hora Despertador:
    {{diaActual.horaDespertador}}</p>

  <div class="input-group mt-3">
    <div class="input-group-prepend">
      <label class="input-group-text" for="inputGroupSelect01">Cambiar Hora</label>
    </div>
  <input v-model.number="diaActual.horaDespertador"
     type="range" min="0" max="23" class="form-control" placeholder="Hora Despertador">
  </div>

  <div class="input-group mt-3">
    <div class="input-group-prepend">
      <label class="input-group-text" for="inputGroupSelect01">Día</label>
    </div>
    <select v-model="diaSeleccionado" class="custom-select">
      <option value="lunes">Lunes</option>
      <option value="martes">Martes</option>
      <option value="miercoles">Miércoles</option>
      <option value="jueves">Jueves</option>
      <option value="viernes">Viernes</option>
      <option value="sabado">Sábado</option>
      <option value="domingo">Domingo</option>
    </select>
  </div>

  <div class="mt-3 text-center">
    <button v-if="diaActual.despertadorPrendido"
            v-on:click="diaActual.despertadorPrendido = false"
            class="btn btn-danger">Apagar</button>
    <button v-else
            v-on:click="diaActual.despertadorPrendido = true"
            class="btn btn-success">Prender</button>
  </div>
</div>

<script>
  new Vue({
    el: '#despertador-app-4',
    computed: {
      diaActual: function () {
        return this.dias[this.diaSeleccionado];
      }
    },
    data: {
      diaSeleccionado: 'lunes',
      dias: {
        lunes: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        martes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        miercoles: {
          despertadorPrendido: true,
          horaDespertador: 8
        },
        jueves: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        viernes: {
          despertadorPrendido: true,
          horaDespertador: 7
        },
        sabado: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
        domingo: {
          despertadorPrendido: false,
          horaDespertador: 8
        },
      }
    }
  })
</script>

Resulta en:

Despertador Prendido

Despertador Apagado

Hora Despertador: {{diaActual.horaDespertador}}

Notaras que dias[diaSeleccionado] cambio a diaActual, pero como diaActual no es un valor, sino un valor "calculado" (computed en ingles) en base a otros dos valores dias y diaSeleccionado, no podemos ponerlo en el campo data, para estos datos calculados, vue nos permite especificarlos en el atributo computed, donde cada campo es un nombre y una función que devuelve su valor.

En nuestro caso:

computed: {
  diaActual: function () {
    return this.dias[this.diaSeleccionado];
  }

Ahora podemos decir diaActual en nuestro HTML y vue lo va a reemplazar por el valor de dias[diaSeleccionado].

Notaras que a diferencia del código en v-on:click en el HTML acá cada variable empieza con this., esto es porque en el HTML de vue las únicas variables que podemos acceder son las que están en el campo data y computed de nuestra aplicación, por eso vue nos hace el favor de ponerle this. adelante a cada nombre, cuando pasamos ese código a javascript, el navegador no sabe a que nos estamos refiriendo, puede ser una variable en la función actual, una variable global o un atributo de nuestra aplicación vue, para especificar que nos referimos a un nombre dentro de nuestra aplicación, tenemos que prefijar los nombres con this (que en ingles se traduce a "esto/este" y siempre se refiere al objeto al que pertenece la función que estamos ejecutando.

El otro cambio, mas allá de algunas mejoras estéticas usando clases de bootstrap, es que le cambiamos el tipo al input de number a range y le especificamos dos atributos nuevos min y max, de esta manera el navegador en lugar de mostrar un campo de texto donde se puede escribir cualquier cosa, muestra un "slider" donde solo se puede especificar un valor en el rango valido.

Con eso ya tenemos nuestro despertador con soporte para alarmas por día, en el camino repasamos vuejs y aprendimos sobre campos calculados y el tipo range del tag input.

Creemos en la Web: Calcular o no calcular, esa es la cuestión

En capítulos anteriores vimos un tipo de dato llamado bool o boolean que puede tomar dos valores, verdadero (true) o falso (false), los cuales son el resultado de operaciones lógicas y de comparación.

En el siguiente capitulo vimos como hacer cálculos, ahora vamos a unir estos dos conceptos usando el resultado de comparaciones y operaciones lógicas para decidir que calculamos y que no, para eso vamos a introducir una nueva expresión llamada if, pero antes veamos un ejemplo.

Vas a poner el despertador, si "el día es un día de semana", entonces pones el despertador a las 8, sino lo dejas apagado.

Esto lo podríamos expresar en javascript así:

if (esDiaDeSemana) {
    ponerDespertador(8);
}

Esta es la versión mas simple, en la cual escribimos la palabra clave if, luego entre paréntesis la condición que va a evaluar a verdadero o falso, luego entre llaves las expresiones que queremos evaluar si la condición es verdadera.

Ese ejemplo asume que el despertador esta apagado, por lo que no necesitamos hacer nada si esDiaDeSemana es falso.

Pero que pasa si queremos apagarlo por las dudas este prendido, no queremos despertarnos temprano por accidente un sábado!

Entonces podemos usar otra palabra clave, veamos un ejemplo:

if (esDiaDeSemana) {
    ponerDespertador(8);
} else {
    apagarDespertadorSiEstaPrendido();
}

En este caso, después de la llave de cierre escribimos la palabra clave else que significa "sino" y entre llaves las expresiones a evaluar si la condición en el if es falsa.

Podemos imaginar que el código de apagarDespertadorSiEstaPrendido es algo como:

if (despertadorPrendido) {
    apagarDespertador();
}

Con esto podemos hacer muchísimas cosas, pero si tu semana es variada y tenes que despertarte a horas distintas según el día, vas a necesitar tener mas de una condición, veamos como seria eso uniendo las dos palabras claves que ya aprendimos y pongamos todo en una función.

function configurarDespertador(diaDeSemana) {
    if (diaDeSemana === "lunes" || diaDeSemana === "miércoles") {
        ponerDespertador(8);
    } else if (diaDeSemana === "martes" || diaDeSemana === "viernes") {
        ponerDespertador(7);
    } else {
        apagarDespertadorSiEstaPrendido();
    }
}

Antes de describir la función por completo, notar que para tener una segunda condición a comprobar después del primer if, escribimos la palabra clave else seguida de la palabra clave if, esto se leería algo así como:

si (condicion1) entonces
    ejecutar bloque1
sino si (condicion2) entonces
    ejecutar bloque2
sino
    ejecutar bloque3.

Definimos la función configurarDespertador que recibe como parámetro el diaDeSemana, y con el tenemos 3 bloques que podemos evaluar:

  • Si es lunes o miércoles: despertador a las 8

  • Si es martes o viernes: despertador a las 7

  • sino, asegurarse que el despertador este apagado

Si te fijas, el despertador no va a sonar los fines de semana ni el jueves, cuando escribimos condiciones complejas o encadenadas hay que fijarse bien que estamos probando todos los casos.

Para poder probar esto con código completo y ver si tomamos la decisión correcta vamos a aprender dos formas de mostrar al usuario de nuestra aplicación el resultado.

Mostrando texto en la consola de desarrolladores con console.log.

Durante el desarrollo nos puede pasar que queremos saber por donde se ejecuto el código o cual es el valor de una variable, para eso podemos usar el objeto console que tiene algunas funciones útiles, la mas útil de ellas es la función log.

Abrí una pagina cualquiera, abrí las herramientas de desarrollo (usualmente F12 la abre), abrí el tab "Consola" (o nombre similar) si no esta abierto en ese, si ves muchas cosas ahí hace click en el icono del tacho de basura para limpiar la consola y escribí:

console.log("hola");

Y apretá enter, deberías ver que la linea siguiente dice "hola", probemos algunos otros ejemplos:

let numero = 42;
console.log(numero);
console.log("numero: " + numero);
console.log("numero", numero);
console.log("numero", numero, true, null, "hola");

A mi me quedo así:

/galleries/cew/if-else/console-log.png

Podrás notar que console.log recibe tantos parámetros como deseemos y los muestra a todos, no hay necesidad de juntarlos todos en un solo valor de tipo texto.

El objeto console tiene otras funciones útiles, proba alguno de los ejemplos anteriores reemplazando la función log con warn, error, info.

console.warn(numero);
console.error("numero: " + numero);
console.info("numero", numero);

Con esto podemos "simular el despertador", sin tener que escribir todo ahora, solo vamos a imprimir que haría.

Pero antes de ir a eso, vamos a ver otra forma de mostrar información, que si bien es simple y medio molesta es una buena herramienta cuando estamos empezando un proyecto.

Como existe el objeto console, que tiene varias funciones relacionadas a la consola de desarrollo, existe el objeto window que tiene funciones relacionadas a la ventana donde esta nuestra pagina, la cantidad de funciones que tiene es impresionante, pero por ahora nos vamos a enfocar en solo tres, la función alert, que nos permite mostrar un mensaje al usuario, la función confirm que nos permite mostrar un mensaje al usuario y el usuario puede responder el clásico "OK" o "Cancelar" y la función prompt que nos permite preguntar algo y el usuario puede responder con texto o cancelar.

Empecemos por la mas fácil, alert:

window.alert("hola");
let numero = 42;
window.alert("numero: " + numero);
/galleries/cew/if-else/alert.png

A diferencia de console.log, window.alert recibe un solo parámetro, si queremos mostrar el valor de múltiples variables tenemos que juntarlas en un solo valor de tipo texto.

Veamos window.confirm:

let respuesta;
// responde una de las dos opciones
respuesta = window.confirm("Seguir?");
console.log('Respuesta', respuesta);

// responde la otra
respuesta = window.confirm("Seguir?");
console.log('Respuesta', respuesta);
/galleries/cew/if-else/confirm.png

la función devuelve true si se selecciono "OK" y false si se selecciono "Cancel".

Por ultimo window.prompt, el cual recibe dos parámetros, el primero es el mensaje a mostrar, el segundo es el valor por defecto para el campo de texto, si no lo especificamos empieza con el texto vació.

// selecciona "Cancel"
respuesta = window.prompt("Día de Semana", "lunes");
console.log('Respuesta', respuesta);

// selecciona "OK"
respuesta = window.prompt("Día de Semana", "lunes");
console.log('Respuesta', respuesta);
/galleries/cew/if-else/prompt.png

La función devuelve null si se selecciono "Cancel" y el texto en el campo de texto si se selecciono "OK".

Probemos combinándolas un poco:

let r1 = window.confirm("Seguir?");
if (r1) {
    window.alert(":)");
} else {
    window.alert(":(");
}

El código te va a preguntar si querés seguir, si respondes OK va a mostrar :), si respondes cancel va a mostrar :(.

Una nota por si estas probando y te da este error o algo parecido:

SyntaxError: redeclaration of let nombreDeVariableAca

Eso es porque declaraste dos veces la misma variable, las variables se declaran una sola vez por función con let, acá estamos en la consola, así que es como una función eterna que ejecuta cada linea que le damos, así que solo tenemos que declarar la variable una vez, después simplemente se siguen usando, sin tener que declararla de nuevo.

Veamos como usaríamos la función window.prompt para configurar nuestro despertador:

let r2;
r2 = window.prompt("Día", "lunes");
if (r2 === null) {
    window.alert("Acción cancelada");
} else {
    configurarDespertador(r2);
}

Acá estamos llamando a configurarDespertador, que definimos mas arriba solo si el usuario ingreso un día y apretó OK.

La función configurarDespertador llama a un par de funciones que todavía no definimos, así que si probas este código te va a dar un error diciendo que apagarDespertadorSiEstaPrendido o ponerDespertador no están definidas, por ahora vamos a usar window.alert y una variables globales para simular el despertador.

Acá va todo el código junto.

// una variable global (fuera de las funciones) para saber si el despertador
// esta prendido o no, inicializado a false, indicando que esta apagado
let despertadorPrendido = false,
    // variable global para saber a que hora esta puesta la alarma
    horaDespertador = 0;

function preguntarDiaYPonerDespertador() {
    let respuesta = window.prompt("Día", "lunes");

    if (respuesta === null) {
        window.alert("Acción cancelada");
    } else {
        configurarDespertador(respuesta);
    }
}

function configurarDespertador(diaDeSemana) {
    if (diaDeSemana === "lunes" || diaDeSemana === "miércoles") {
        ponerDespertador(8);
    } else if (diaDeSemana === "martes" || diaDeSemana === "viernes") {
        ponerDespertador(7);
    } else {
        apagarDespertadorSiEstaPrendido();
    }
}

function prenderDespertadorSiEstaApagado() {
    // si no esta prendido
    if (!despertadorPrendido) {
        // lo prendemos
        despertadorPrendido = true;
        console.log('despertador prendido');
    } else {
         console.log('despertador ya estaba prendido');
    }
}

function apagarDespertadorSiEstaPrendido() {
    // si esta prendido
    if (despertadorPrendido) {
        // lo apagamos
        despertadorPrendido = false;
        console.log('despertador apagado');
    } else {
        console.log('despertador ya estaba apagado');
    }
}

function ponerDespertador(hora) {
    prenderDespertadorSiEstaApagado();
    console.log('nueva hora para despertador', hora);
    horaDespertador = hora;
}

Te recomiendo que escribas el código en lugar de copiar y pegarlo, de esa forma te vas a ir acostumbrando a escribir los paréntesis, llaves y puntos y coma en su lugar y a entender los errores cuando te equivocaste en algo.

Luego de declarar las variables y funciones llama preguntarDiaYPonerDespertador(); y proba con distintos días varias veces viendo que se imprime en la consola.

Como ejercicio queda comprobar que el día ingresado es un día valido, sino informarlo con window.alert y no intentar configurar el despertador.

Como ayuda, te recomiendo que declares una nueva función esDiaValido, que uses if y else if para comprobar que el día pasado como parámetro es un día valido, si lo es devolvé true (return true;), en el else devolvé false (return false;).

Riak Core Tutorial Part 9: Persistent KV with leveled backend

The content of this chapter is in the 07-leveled-kv branch.

https://gitlab.com/marianoguerra/tanodb/tree/07-leveled-kv

Implementing it

Until now we have an in memory key-value store, what do we have to do to make it a persistent one?

We would need to implement a new kv backend, that implements the same API as tanodb_kv_ets but using a library that persists to disk.

For this we are going to use leveled a pure erlang implementation of leveldb.

Being pure erlang means it's easy to build on any platform and easy to understand and contribute since it's all erlang!

The changes will involve making room for configurable KV backends, for that we will keep the backend module in a field called kv_mod in the vnode state:

-record(state, {partition, kv_state, kv_mod}).

On init we will pass an extra field to the KV backend init function with the base path where it can safely store files without clashing with other vnodes in the same node:

init([Partition]) ->
        DataPath = application:get_env(tanodb, data_path, "."),
        KvMod = tanodb_kv_leveled,
        {ok, KvState} = KvMod:new(#{partition => Partition,
                                                                data_path => DataPath}),
        {ok, #state { partition=Partition, kv_state=KvState, kv_mod=KvMod }}.

We are getting the base path to store data from an environment variable (tanodb.data_path), to make it configurable we need to add it to our cuttlefish schema on priv/01-tanodb.schema:

%% @doc base folder where data is stored
{mapping, "paths.data", "tanodb.data_path", [
  {datatype, directory},
  {default, "{{platform_data_dir}}/vnodes"}
]}.

Then we need to replace all the places in tanodb_vnode where we used tanodb_kv_ets to use the value of kv_mod from the state record.

On rebar.config we need to add the leveled dependency, since it doesn't have any release and it's not on hex.pm we will reference the master branch from the github repo:

{deps, [cowboy, jsx, recon,
        {riak_core, {pkg, riak_core_ng}},
        {leveled, {git, "https://github.com/martinsumner/leveled.git", {branch, "master"}}}
]}.

We specify in the release to load leveled and its dependency lz4:

{relx, [{release, { tanodb , "0.1.0"},
                 [tanodb,
                  cuttlefish,
                  cowboy,
                  {leveled, load},
                  {lz4, load},
                  jsx,
                  sasl]},

At this point in time, to be able to compile leveled on Erlang 20.3, we need to add an override to remove the warnings_as_errors option in erl_opts:

{override, leveled,
        [{erl_opts, [{platform_define, "^1[7-8]{1}", old_rand},
                {platform_define, "^R", old_rand},
                {platform_define, "^R", no_sync}]}]}

The code for apps/tanodb/src/tanodb_kv_leveled.erl:

-module(tanodb_kv_leveled).
-export([new/1, get/3, put/4, delete/3, keys/2, dispose/1, delete/1,
                 is_empty/1, foldl/3]).

-include_lib("leveled/include/leveled.hrl").

-record(state, {bookie, base_path}).

new(#{partition := Partition, data_path := DataPath}) ->
        Path = filename:join([DataPath, "leveled", integer_to_list(Partition)]),
        {ok, Bookie} = leveled_bookie:book_start(Path, 2000, 500000000, none),
        State = #state{bookie=Bookie, base_path=Path},
        {ok, State}.

put(State=#state{bookie=Bookie}, Bucket, Key, Value) ->
        R = leveled_bookie:book_put(Bookie, Bucket, Key, Value, []),
        {R, State}.

get(State=#state{bookie=Bookie}, Bucket, Key) ->
        K = {Bucket, Key},
        Res = case leveled_bookie:book_get(Bookie, Bucket, Key) of
                          not_found -> {not_found, K};
                          {ok, Value} -> {found, {K, Value}}
                  end,
        {Res, State}.

delete(State=#state{bookie=Bookie}, Bucket, Key) ->
        R = leveled_bookie:book_delete(Bookie, Bucket, Key, []),
        {ok, State}.

keys(State=#state{bookie=Bookie}, Bucket) ->
        FoldHeadsFun = fun(_B, K, _ProxyV, Acc) -> [K | Acc] end,
        {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                                                        {foldheads_bybucket,
                                                                ?STD_TAG,
                                                                Bucket,
                                                                all,
                                                                FoldHeadsFun,
                                                                true, true, false}),
        Keys = FoldFn(),
        {Keys, State}.

is_empty(State=#state{bookie=Bookie}) ->
        FoldBucketsFun = fun(B, Acc) -> [B | Acc] end,
        {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                                                                                                           {binary_bucketlist,
                                                                                                                ?STD_TAG,
                                                                                                                {FoldBucketsFun, []}}),
        IsEmpty = case FoldFn() of
                                  [] -> true;
                                  _ -> false
                          end,
        {IsEmpty, State}.

dispose(State=#state{bookie=Bookie}) ->
        ok = leveled_bookie:book_close(Bookie),
        {ok, State}.

delete(State=#state{base_path=Path}) ->
        R = remove_path(Path),
        {R, State}.

foldl(Fun, Acc0, State=#state{bookie=Bookie}) ->
        FoldObjectsFun = fun(B, K, V, Acc) -> Fun({{B, K}, V}, Acc) end,
        {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie, {foldobjects_allkeys,
                                                                                                                                ?STD_TAG,
                                                                                                                                {FoldObjectsFun, Acc0},
                                                                                                                                true}),
        AccOut = FoldFn(),
        {AccOut, State}.

% private functions

sub_files(From) ->
        {ok, SubFiles} = file:list_dir(From),
        [filename:join(From, SubFile) || SubFile <- SubFiles].

remove_path(Path) ->
        case filelib:is_dir(Path) of
                false ->
                        file:delete(Path);
                true ->
                        lists:foreach(fun(ChildPath) -> remove_path(ChildPath) end,
                                                  sub_files(Path)),
                        file:del_dir(Path)
        end.

Trying it

From the user perspective nothing changed other than the fact that the data will persist between restarts.

To test it redo the "Trying it" sections from the Handoff and Coverage Calls chapters.

Creemos en la Web: Nombrando cosas

En la sección anterior (Creemos en la Web: Una potente calculadora) aprendimos a usar javascript como una calculadora, probemos usarla para eso convirtiendo de grados Celsius a Fahrenheit.

En la mayoría de los países se usan los grados centígrados para temperatura, pero en algunos otros se usan los grados Fahrenheit, buscando en google como es la formula de conversión, google me dice esto:

°F = °C x 1.8 + 32

Donde °F significa "el resultado de la conversión, grados fahrenheit" y °C significa "el valor en grados centígrados a convertir".

Como podemos ver, la formula "nombra" valores que van a variar según que queremos convertir, probemos calcular cuanto es 36 grados centígrados a fahrenheit:

36 * 1.8 + 32
< 96.8

El código es bastante similar, pero tiene un par de problemas:

Primero, si nos encontramos con la expresión 36 * 1.8 + 32 en el medio de otro código, como sabemos que partes son "parámetros", es decir, valores que podemos cambiar y cuales son valores fijos de la formula.

Segundo, como sabemos formula de que es? puede ser cualquier calculo.

Tercero, si queremos usar esa formula muchas veces para distintos valores, tenemos que escribir la formula cada vez? Osea que tenemos que recordarla siempre o buscarla siempre? Esto puede hacer que en algún momento nos equivoquemos y la escribamos mal o si necesitamos hacer un cambio tenemos que buscar todas las veces que la usamos y cambiarla.

Todos estos son indicios de lo mismo, nos gustaría ponerle un nombre a la formula y cada vez que la queramos usar simplemente mencionamos el nombre y le proveemos los valores que requiera. En este caso el nombre podría ser "centigradosAFahrenheit".

... silencio ...

Te preguntas porque ese nombre todo junto?

Resulta que en javascript podemos nombrar cualquier cosa siempre que el nombre cumpla con un par de condiciones:

  • Tiene que empezar con una letra, minúscula o mayúscula del alfabeto ingles

  • Luego puede tener cualquier combinación de letras minúsculas, mayúsculas o números

Nombres validos:

  • A

  • b

  • Ab

  • aB

  • a1

  • diaDelMes

  • parametro9

Nombres inválidos:

  • 1a

    • empieza con numero

  • a#

    • tiene un carácter que no es ni una letra ni un numero

  • centígrados a fahrenheit

    • tiene espacios y tilde, no esta permitido

  • año

    • tiene ñ

Otro detalle es que en javascript, las formulas se llaman funciones, como en matemáticas, aunque las funciones de javascript son mas generales que las funciones en matemáticas, funcionan de una manera similar:

  • Una función tiene un nombre

  • Una función recibe cero o mas valores de entrada o parámetros

  • Una función devuelve un valor de salida

Veamos como definir una función bien simple en javascript, una función que no recibe ningún parámetro y siempre devuelve el número 7:

function siete() {
    return 7;
}

Vamos por partes:

Para definir una función en javascript, empezamos escribiendo la palabra "function", que significa función en ingles.

Luego indicamos el nombre de la función, en este caso "siete".

Luego viene la lista de parámetros entre paréntesis, separados por coma si hay mas de uno, en este caso no necesitamos ningún parámetro, así que simplemente abrimos y cerramos los paréntesis.

Luego viene "el cuerpo" de la función, que es donde hacemos los cálculos necesarios, cada calculo se separa del siguiente con un punto y coma ";".

Cuando queremos terminar de ejecutar la función y devolver un valor lo indicamos empezando la linea con la palabra "return" (retornar en ingles) y el valor que queremos devolver, en este caso el numero 7.

Como usamos nuestra primera función? la nombramos y le indicamos los parámetros que le queremos pasar, en este caso no hay parámetros así que de nuevo es simplemente abrir y cerrar paréntesis.

siete()
< 7

Volviendo al caso de la formula de conversión, tenemos un parámetro que es el valor en centígrados que queremos convertir.

Como ese parámetro va a tomar distintos valores para cada conversión, le vamos a poner un nombre, propongo ponerle "gradosC".

Veamos de definir nuestra función:

function centigradosAFahrenheit(gradosC) {
    return gradosC * 1.8 + 32;
}

Como veras seguimos la misma estructura que antes, la palabra "function", seguida del nombre de la función a definir, seguida de la lista de parámetros entre paréntesis, en este caso uno solo, llamado "gradosC", seguido del cuerpo de la función entre llaves.

El cuerpo de la función tiene una sola linea que "retorna" el resultado de calcular gradosC * 1.8 + 32.

Como veras cuando queremos usar un parámetro simplemente lo nombramos y javascript reemplaza el valor que se le asigno en la llamada en ese lugar, por lo que si llamamos a la función con el valor 36:

centigradosAFahrenheit(36)

Nos devuelve:

< 96.8

Lo que hace javascript es reemplazar el valor del parámetro en esta llamada dentro del cuerpo de la función, así que:

return gradosC * 1.8 + 32;

En esta llamada se convierte en:

return 36 * 1.8 + 32;

Vamos viendo que nombrar cosas hace todo mas legible y reusable, dentro de una función podemos nombrar cualquier valor si hace las cosas mas claras.

En este ejemplo es mas o menos claro, pero si quisiéramos ser mas explícitos podríamos ponerle un nombre al resultado como la formula inicial:

°F = °C x 1.8 + 32

Eso lo hacemos con otra palabra especial, cuando queremos nombrar una función nueva usamos la palabra "function", cuando queremos nombrar un parámetro simplemente lo nombramos en la lista de parámetros, pero cuando queremos nombrar un valor en el cuerpo de una función, tenemos que usar la palabra "let", una palabra que podríamos traducir como "sea", veamos su uso así queda mas claro:

function centigradosAFahrenheit1(gradosC) {
    let gradosF = gradosC * 1.8 + 32;
    return gradosF;
}

La linea:

let gradosF = gradosC * 1.8 + 32;

Podríamos traducirla como "sea gradosF igual a gradosC * ...".

En la siguiente linea nombramos gradosF, que es reemplazado por el valor que se le asigno en la linea anterior.

Con esto aprendimos que en javascript hay al menos 3 cosas que podemos nombrar:

  • Funciones: una forma de reusar pedazos de código sin repetirnos

  • Parámetros: una forma de indicar partes del código que pueden variar

  • Variables: una forma de nombrar cálculos intermedios sin repetirnos

Estas 3 formas de nombrar nos permiten hacer nuestro código mas claro, hay un dicho muy conocido en la programación que dice:

Hay solo dos cosas difíciles en la programación: nombrar cosas e invalidar caches.

Ya aprendimos a nombrar cosas, lo difícil es ponerle el nombre indicado a cada cosa para que luego cuando se lea el código de nuevo quede clara la intención.

Sobre caches vamos a aprender eventualmente, alguna vez cuando una pagina no funcionaba bien alguien te habrá dicho que limpies el cache de la computadora, esa es la razón :)