Introducción

Hoy vengo con un caso práctico para afianzar los conocimientos que hemos ido viendo a lo largo de estos artículos.

Vamos a crear la típica página web para crear tareas que se suele hacer cuando empiezas con un framework. Siempre recomiendo ir creando ejemplos de este tipo según aprendes cualquier lenguaje porque muchas cosas no las entiendes del todo hasta que no las pones en práctica.

🗺️ Hoja de ruta

  • [ ] 🏗️ Crear el proyecto de Vue
  • [ ] ✨ Crear el formulario para crear tareas
  • [ ] ✨ Mostrar las tareas
  • [ ] ✨ Hacer que al pulsar las tareas se marquen como completadas
  • [ ] 💄 Crear estilos para las tareas

Creando el proyecto de Vue

Como vimos anteriormente, la manera más sencilla de crear un proyecto de Vue es utilizando su asistente de línea de comandos llamado Vue CLI. Si todavía no lo tienes instalado en tu equipo ejecuta:

npm install -g @vue/cli

Ahora ejecuta el comando para crear el proyecto con el nombre que quieras, en mi caso “tareas”.

vue create tareas

Te preguntará si quieres crear el proyecto directamente o si quieres elegir las opciones manualmente. Yo voy a crear el proyecto mediante el método default pero tú puedes seleccionar cada cosa por separado si lo prefieres, te dejo este artículo donde explico cada opción:

Tutorial Vue CLI

Listo, proyecto creado y primera tarea completada.

[X] 🏗️ Crear el proyecto de Vue

El comando habrá creado una carpeta en el directorio en el que te encontraras en la terminal al realizar ejecutar el comando. Para acceder simplemente ejecuta cd tareas

Te recomiendo que cada vez que termines una tarea, hagas commit con los cambios para tenerlo guardado por si quieres verlos o volver a un punto anterior.

Ahora ejecuta npm run serve dentro del proyecto para comprobar que el proyecto se ha creado correctamente.

Creando el formulario con Vue

Vamos con la siguiente tarea, la de crear el formulario y para ello vamos a seguir un ciclo iterativo, es decir, poco a poco añadimos funcionalidad y por el momento no nos preocupamos por cómo se vea o si es feo o bonito.

De lo que se trata es de que funcione y más adelante mejoraremos el apartado visual.

Lo primero que voy a hacer es borrar completamente el componente HelloWorld.vue que viene por defecto al crear el proyecto.

Dentro de la carpeta /src/components voy a crear un archivo llamado TodoList.vue con la estructura básica de un componente:

<template>
</template>

<script>
export default {

}
</script>

<style>
</style>

Para que todo sea más sencillo en este componente irá toda la lógica de la aplicación.

En posteriores capítulos veremos cómo separar funcionalidad y cómo pasar información de un componente a otro pero por el momento vamos a hacer las cosas sencillas para aprender.

Ante de seguir tenemos que sustituir dentro del componente App.vue el componente de HelloWorld por el que acabamos de crear. Quedaría así:

<template>
  <div id="app">
    <todo-list />
  </div>
</template>

<script>
import TodoList from './components/TodoList.vue'

export default {
  name: 'app',
  components: {
    TodoList
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Recuerda que vue automáticamente hace el cambio de PascalCase a kebab-case en la vista. Es decir al poner el nombre TodoList, para usarlo en el template de otro componente tendrás que usar todo-list.

Hora de crear el formulario. El formulario va a constar de un elemento <form> y dentro un input para escribir la tarea y un botón submit.

Recogeremos el valor del input y lo guardaremos en una variable. Cuando el usuario haga clic sobre el input almacenaremos la tarea en el array de tareas.

Dentro del archivo TodoList que hemos creado antes por el momento he puesto esto:

<template>
  <form id="app">
    <label class="label" for="task">Nueva tarea: </label>
    <input type="text" v-model="newTask" id="task">
      {{newTask}}
    <input type="submit" value="Crear tarea">
  </form>
</template>

<script>
export default {
  data: () => ({
    newTask: ""
  })
};
</script>

<style>
</style>

Dentro del data he creado la variable newTask en la que se almacenará el valor que meta el usuario en el input para crear tareas.

Encima del botón de submit he puesto la variable de forma temporal para que veas que se actualiza en tiempo real su valor al escribir sin tener que hacer nada.

Lo que queda para terminar esta tarea es poder almacenar la tarea que escriba el usuario.

Para ello vamos a crear en el data una variable para almacenar todas las tareas (por defecto se crea vacío) y un método conectado el submit para que al darle al botón se guarde en el array. Vamos a ello:

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea: </label>
    <input type="text" v-model="newTask" id="task">
    <input type="submit" value="Crear tarea">
  </form>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: false
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    }
  }
};
</script>

<style>
</style>

¿Recuerdas que cuando vimos los métodos hablamos de un evento @click que sirve para capturar eventos de clic en elementos del html? Si no lo recuerdas echa un vistazo a este artículo sobre ḿetodos y computadas en Vue

Bueno pues hay otro evento especial para los formularios para capturar cuando el usuario hace submit en un formulario.

Como en todos los eventos, para recoger el valor se hace con el @ delante y llamando al método que queremos que se ejecute.

Este evento tiene una particularidad y es que puedes añadir el .prevent detrás para evitar que la página se recargue cuando hacemos submit.

Dentro del método que llamamos de createTask lo que hago es crear una variable local llamada task en la que defino la estructura que tendrán todas las tasks.

Cada tarea será un objeto con dos parámetros, “texto” que será un string con la descripción de la tarea (lo que el usuario mete en el input) y “completed”, un booleano para saber si está completada (por defecto a false).

Como las propiedades del data son reactivas lo único que tenemos que hacer es meter dentro del parámetro text de la task el valor de la variable newTask que tendrá el valor introducido por el usuario.

Luego lo que se hace es meter la tarea dentro del array de tareas definido en la sección data y limpiar el valor de newTask para que cada vez que se cree una el input se limpie.

Pues listo, otra tarea completada, hora de hacer commit y pasar a la siguiente.

[X] ✨ Crear el formulario para crear tareas

Mostrar las tareas creadas

Mostrar las tareas no debería que ser muy complicado, solo tenemos que mostrar el array con la tareas (usando un bucle v-for en la vista) y definir una clase dependiendo de si la tarea se ha completado o no. Vamos con ello.

Para pintar las tareas lo hacemos con un v-for:

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea:</label>
    <input type="text" v-model="newTask" id="task" />
    <input type="submit" value="Crear tarea" />
    <ul>
      <li v-for="(task, i) in tasks" :key="'task' + i">{{task.text}}</li>
    </ul>
  </form>
</template>

Recuerda añadir el parámetro único key para que vue pueda identificar cada uno de los elementos del bucle.

Por último dentro del bucle accedemos al texto de la tarea accediendo a un propiedad text (o el que hayas definido al crear tareas).

Vamos ahora con los estilos para que el usuario sepa si la tarea está completada o no.

Si recuerdas capítulos anteriores, puedes definir clases de en los elementos HTML de forma dinámica dependiendo de si una variable está a true o false.

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea:</label>
    <input type="text" v-model="newTask" id="task" />
    <input type="submit" value="Crear tarea" />
  </form>
    <ul>
      <li v-for="(task, i) in tasks" :key="'task' + i" :class="{completed: task.completed}">{{task.text}}</li>
    </ul>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: true
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    }
  }
};
</script>
<style scoped>
.completed {
  text-decoration: line-through;
  color: grey;
}
</style>

La clase completed se añadirá dependiendo de si el objeto task que está pintando el bucle tiene la propiedad completed a true o false.

Para que veas que funciona he puesto en el método de crear tareas que por defecto las ponga con el parámetro completed a true para que todas las tareas se creen ya completadas.

Por último he añadido estilos a la clase completed. En mi caso he hecho que se pinte el texto tachado y las letras de color gris más claro para que se diferencie más, pero tu puedes poner los estilos que quieras.

Tarea completada.

[X] ✨ Mostrar las tareas

Completando tareas

Toca hacer que las tareas se completen. En mi caso he decidido que se completen al pulsar sobre ellas.

Para este ejemplo lo voy hacer con el evento de click que ya conocemos pero lo correcto y accesible sería crear un botón al lado de cada tarea.

<template>
  <form id="app" @submit.prevent="createTask">
    <label class="label" for="task">Nueva tarea:</label>
    <input type="text" v-model="newTask" id="task" />
    <input type="submit" value="Crear tarea" />
  </form>
  <ul>
    <li
      v-for="(task, i) in tasks"
      :key="'task' + i"
      :class="{completed: task.completed}"
      @click="completeTask(task.text)"
    >{{task.text}}</li>
  </ul>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: true
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    },
    completeTask(taskText) {
      for (let i = 0; i < this.tasks.length; i++) {
        let task = this.tasks[i];
        if (taskText === task.text) {
          task.completed = !task.completed;
        }
      }
    }
  }
};
</script>
<style scoped>
.completed {
  text-decoration: line-through;
  color: grey;
}
</style>

Veamos lo que he cambiado.

En primer lugar, he llamado al evento @click dentro de cada tarea en la vista. El evento de click llama a la función completeTask pasando el texto de la tarea (lo mejor hubiera sido asignar a cada tarea un id y pasar a este función el id de la tarea a completar).

Ya en la función, simplemente lo que hago es recorrer todas las tareas en busca de la tarea que ha pulsado el usuario. Si se encuentra la tarea hace que su parámetro completed se invierta, de tal forma que si no se ha completado se complete y viceversa.

DISCLAIMER. Sí, se que esta solución no es ni de lejos la más óptima y correcta, pero he optado por esto para que sea sencillo de entender. Por ejemplo no hace falta recorrer toda la lista para encontrar una tarea, Puedes añadir un break cuando la encuentre para que el bucle no continúe o puedes usar la función find de javascript.

Otra tarea más completada, falta una.

[X] ✨ Hacer que al pulsar las tareas se marquen como completadas

Estilos

Ya solo queda añadir los estilos CSS que quieras, a tu gusto.

Aquí no hay mucho que explicar, yo he optado por añadir estos. Te dejo como ha quedado todo el componente:

<template>
  <div class="task-list">
    <h1>{{tasks.length}} Tasks</h1>
    <form class="form" @submit.prevent="createTask">
      <label class="label" for="task">Nueva tarea:</label>
      <input class="input" type="text" v-model="newTask" id="task" />
      <input class="button" type="submit" value="Crear tarea" />
    </form>
    <ul class="list">
      <li
        class="task"
        v-for="(task, i) in tasks"
        :key="'task' + i"
        :class="{completed: task.completed}"
        @click="completeTask(task.text)"
      >{{task.text}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data: () => ({
    newTask: "",
    tasks: []
  }),
  methods: {
    createTask() {
      let task = {
        text: this.newTask,
        completed: false
      };
      this.tasks.push(task);
      this.newTask = "";
      console.log(this.tasks);
    },
    completeTask(taskText) {
      for (let i = 0; i < this.tasks.length; i++) {
        let task = this.tasks[i];
        if (taskText === task.text) {
          task.completed = !task.completed;
        }
      }
    }
  }
};
</script>
<style scoped>
.task-list {
  width: 800px;
  max-width: 100%;
  margin: 0px auto;
}
.form {
  background: white;
  border-radius: 12px;
  padding: 30px;
  box-shadow: 0px 10px 22px -1px rgba(0,0,0,0.25);
  margin-top: 10px;
}
.label {
  display: block;
  margin-bottom: 10px;
}
.input {
  height: 35px;
}
.button {
  margin-left: 20px;
  height: 35px;
  border: none;
  border-radius: 5px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, .2);
  background-color: #2ecc71;
  color: #ecf0f1;
  cursor: pointer
}
.list {
  margin-top: 40px;
}
.task {
  cursor: pointer;
  margin: 10px 0;
}
.completed {
  text-decoration: line-through;
  color: lightgrey;
}
</style>

Así quedaría todo:

El diseño lo he hecho en muy rápido en 2 minutos, no me lo tomes en cuenta.

Pues todo listo, te dejo el proyecto subido a Codesanbox para que puedas jugar con él:

Proyecto completo en codesabox

Te dejo un par de ejercicios como deberes por si quieres experimentar por tu cuenta:

  • [ ] 🗑️ Botón dentro de cada tarea para poder eliminarlas completamente.
  • [ ] ✏️ Botón encima de la lista de tareas para poder marcar o desmarcar todas como completadas

Conclusiones

Espero de verdad que te haya gustado este ejemplo. Yo creo que este tipo de ejercicios vienen muy bien cuando estás aprendiendo porque no es solo teoría y se ven las cosas de otra forma.

No te quedes solo con este ejemplo, con lo que ya sabes deberías ser ya capaz de hacer cosas bastantes interesantes. Te animo a que intentes crear otros proyectos con lo que ya sabes para probar tus conocimientos.

En los siguientes episodios seguiremos investigando más funcionalidades de Vue para que puedas hacer cosas más increíbles.