Creación de una app de notas Markdown con VueJS

Foto Alejandro

February 3, 2021 Compartir

vue tutoriales

Creación de una aplicación para gestionar notas en formato markdown y previsualizarlas en tiempo real con VueJS.

Código fuente

Prerequisitos

Para crear nuestra aplicación utilizaremos el framework VueJS haciendo uso de algunas de sus caractísticas fundamentales como enlace bidireccional de datos con elementos del DOM, propiedades computadas, renderizado de listas, métodos, gestión de eventos nativos, asignación dinámica de clases y estilos, renderizado condicional entre otras.

Debido a que el objetivo de este artículo es presentar las características básicas de Vue mencionadas, configuramos nuestro proyecto de la forma más sencilla, sin utilizar ninguna plantilla de proyecto de vue-cli ni webpack. Trabajaremos unicamente en un script javascript el cual será enlazado en el documento index.html de la aplicación, así como la respectiva hoja de estilos css.

Markdown notebook with VueJS

Metas

La aplicación ofrecerá las siguientes funcionalidades:

Para esto la interfaz de nuestra aplicación estará dividida en tres secciones:

Markdown notebook with VueJS

Un editor de notas básico

Comenzaremos el desarrollo de nuestra aplicación con una característica muy simple que permita de edición una única nota en markdown. Para ello se mostrará un editor de texto a la izquierda y la vista previa de la nota a la derecha. Más adelante, la convertiremos en una aplicación que soporte la creación de múltiples notas.

  1. En primer lugar, crearemos un archivo que llamaremos index.html, el cual contendrá la estructura HTML inicial de nuestra aplicación:
<html>
  <head>
    <title>Markdown Notebook</title>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
  </head>
  <body>
    <div id="notebookApp"></div>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="script.js"></script>
  </body>
</html>

En el cuerpo del documento hemos incluído un elemento <div id="notebook"></div> que será el contenedor de nuestra instancia de Vue.

  1. Ahora crearemos el archivo Javascript en donde escribiremos la lógica que hará toda la magia. El archivo debe tener el mismo nombre que hemos utilizado en nuestro HTML, en la línea en donde importamos este archivo, que en nuestro caso es script.js. En este momento lo único que haremos en este archivo será crear la instancia de Vue la cual debemos enlazar a nuestro HTML a través del ID que hemos asignado al elemento div principal, que en nuestro caso es notebookApp:
new Vue({
  el: "#notebookApp",
});
  1. Ahora, en nuestra instancia de Vue, crearemos un nuevo objeto data el cual tendrá una propiedad llamada content, la cual guardará el contenido de la nota que estaremos editando:
new Vue({
  el: "#notebookApp",

  data() {
    return {
      content: "",
    };
  },
});

Hoja de estilos

En nuestra aplicación utilizaremos la hoja de estilos que puedes descargar del repositorio del proyecto.

El editor de notas

Ahora que tenemos la estructura básica de nuestra aplicación, el siguiente paso será agregar el editor de texto. Para ello usaremos un elemento textarea, el cual será creado dentro de un elemento section. En el elemento textarea agregaremos la directiva v-model, la cual nos permite crear un enlace bidireccional entre el contenido del textarea y la propiedad content de nuestra instancia de Vue:

<div id="notebookApp">
  <section class="main">
    <textarea v-model="content"></textarea>
  </section>
</div>

Ahora, cualquier cambio que se realice en el textarea será reflejado automáticamente en la propiedad content de la data de nuestra aplicación.

El panel de vista previa

Para compilar el contenido de la nota de lenguaje markdown a HTML válido, utilizaremos una biblioteca llamada Marked, la cual debemos incluir en nuestro proyecto:

<!-- index.html -->

<!-- La llamada a Vue -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<!-- La llamada a la librería Marked -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

Marked es muy fácil de usar, solo se le debe pasar como único argumento el contenido del texto en formato markdown a la functión marked la cual retornará el HMTL correspondiente.

Con esta librería incluída en nuestro proyecto, ya podemos mostrar en tiempo real el HMTL renderizado de nuestra nota markdown. Para ello, primero debemos crear el elemento en nuestro archivo index.html que se encargará de mostrar nuestra nota renderizada.

Pero antes debemos crear una propiedad computada que llamaremos notePreview. Ella se encargará de utilizar la función marked para renderizar el contenido de la propiedad content cada vez que ésta sea modificada:

computed: {
    notePreview () {
        return marked(this.content)
    }
}

Ahora podemos utilizar nuestra nueva propiedad computada en el archivo index.html:

<aside class="preview" v-html="notePreview"></aside>

Hasta el momento los archivos index.html y script.js contienen el siguiente código:

<!-- index.html -->
<html>
  <head>
    <title>Markdown Notebook</title>
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet"
    />
  </head>
  <body>
    <div id="notebookApp">
      <section class="main">
        <textarea v-model="content"></textarea>
      </section>

      <aside class="preview" v-html="notePreview"></aside>
    </div>

    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
new Vue({
    el: "#notebookApp",
    data() {
        return {
            content: "",
        };
    },
    computed: {
        notePreview() {
            return marked(this.content);
        },
    },
});

Y nuestra aplicación luce de esta forma:

Markdown notebook with VueJS

Soportar múltiples notas

Ahora mejoraremos nuestra aplicación para que permita crear múltiples notas. Para ello agregaremos un nuevo panel lateral a la izquierda que contendrá la lista de notas.

Listado de notas

En primer lugar, en nuestro archivo index.html crearemos el panel lateral que va a contener la lista de notas. Lo haremos agregando un nuevo elemento aside justo antes de nuestra sección principal:

<div id="notebookApp">
    <!-- Panel lateral izquierdo -->
    <aside class="side-bar"></aside>

    <section class="main">
        <textarea v-model="content"></textarea>
    </section>

    <aside class="preview" v-html="notePreview"></aside>
</div>

En la instancia de Vue agregaremos una nueva propiedad que llamaremos notes, que corresponde al array de objetos que contendrá las notas de la aplicación:

...
data () {
    return {
        content: '',
        notes: []
    }
},
...

Método para crear una nueva nota

Cada una de las notas creadas será un objeto que tendrá la siguiente información:

Agregaremos un nuevo método que llamaremos addNote, el cual creará un nuevo objeto note y lo agregará al listado de notas:

methods: {
    addNote () {
        const time = Date.now();

        // Default new note
        const note = {
            id: String(time),
            title: 'Título nota ' + (this.notes.length + 1),
            content: '**Hola!** 👋   \n\nEste notebook está utilizando **markdown** para darle formato a tus notas!',
            created: time,
        };

        // Add to the notes list
        this.notes.push(note);
    }
}

Hemos decidido utilizar una marca de tiempo con la fecha-hora actual (representada como la cantidad de milisegundos transcurridos desde el 1 de enero de 1970, const time = Date.now()), ya que es una forma conveniente de obtener un identificador único para cada nota. También hemos establecido algunos valores iniciales como el título de la nota y un contenido por defecto, así como la fecha de creación de la nota. Esta información la asignará el método addNote automáticamente cada vez que una nueva nota sea creada.

Botón y evento click (v-on)

Ahora necesitaremos un botón para llamar al método recién creado addNote. Crearemos un nuevo elemento button dentro de un elemento div al cual le asignaremos la clase css toolbar:

...
<aside class="side-bar">
    <div class="toolbar">
        <button>
            <i class="material-icons">add</i> Nueva nota
        </button>
    </div>
</aside>
...

Este botón ahora debe llamar al método addNote cuando el usuario haga click sobre él, esto lo lograremos agregando la directiva v-on con su respectiva referencia al evento click y el método mencionado:

...
<button v-on:click="addNote">
    <i class="material-icons">add</i> Nueva nota
</button>
...

Ahora que nuestro botón está funcionando podemos usarlo para agregar algunas notas. No podremos ver las notas aún, pero podemos confirmar su funcionamiento abriendo el panel de inspección y revisar los cambios a través de la herramienta DevTools:

Markdown notebook with VueJS

Mostrando el listado de notas (v-for)

Mostaremos nuestro listado de notas justo debajo del botón recién creado.

  1. Añadimos un nuevo elemento div con la clase css notes:
...
<button v-on:click="addNote">
    <i class="material-icons">add</i> Nueva nota
</button>
<div class="notes">
    <!-- Listado de notas aquí -->
</div>
...

Ahora queremos mostrar un listado con múltiples elementos div, uno por cada nota. Para lograr esto, necesitamos utilizar la directiva v-for, la cual recibe una expresión Javascript especial de la forma: item of items, la cual le permitirá iterar a través de los ítems de un array u objeto y acceder a cualquier valor de cada ítem durante su recorrido:

...
<div class="notes">
    <div class="notes">
        <div class="note" v-for="note of notes">{{ note.title }}</div>
    </div>
</div>
...

Deberíamos ver nuestras notas listadas debajo del botón. Podemos agregar algunas notas usando el botón y veremos cómo el listado ¡se actualiza automáticamente! 😍:

Markdown notebook with VueJS

Seleccionar una nota

Lo siguiente que implementaremos será la funcionalidad que permita al usuario seleccionar una nota. Al hacerlo, la nota seleccionada se convertirá en el contexto actual para el panel central (que permitirá editar la nota actual) y el panel derecho (que mostrará una vista previa de la nota selecionada).

  1. Agregaremos una nueva propiedad al objeto data que llamaremos selectedId, la cual almacenará el ID de la nota actualmente seleccionada:
data () {
    return {
        content: '',
        notes: [],
        selectedId: null,
    }
},
  1. Necesitaremos un nuevo método que será invocado cuando el usuario haga click sobre una nota, a este método lo llamaremos selectNote:
...
methods: {
    selectNote (note) {
        this.selectedId = note.id
    },
}
...
  1. De la misma forma como lo hicimos con el botón para agregar una nueva nota, ahora escucharemos el evento click a través de la directiva v-on que asociaremos al elemento div que corresponde cada nota del listado de notas:
<div class="notes">
    <div class="notes">
        <div 
            class="note" 
            v-for="note of notes"
            v-on:click="selectNote(note)"
        >{{ note.title }}</div>
    </div>
</div>

Ahora la propiedad selectedId se actualizará automáticamente al hacer click sobre cualquiera de las notas creadas.

Mostrar la nota actualmente seleccionada

Ahora que ya sabemos cuál es la nota que se encuentra actualmente seleccionada, podemos reemplazar la propiedad content que creamos al comienzo. En este punto necesitaremos una nueva propiedad computada que nos permita acceder a la nota que esté actualmente seleccionada y a todo su contenido.

  1. Añadiremos una nueva propiedad computada que llamaremos selectedNote, la cual retornará el objeto note que corresponda al ID que coincida con la propiedad selectedId:
computed: {
    selectedNote () {
        return this.notes.find(note => note.id === this.selectedId)
    },
}
  1. Necesitamos reemplazar la propiedad content por selectedNote.content:
<section class="main">
    <textarea v-model="selectedNote.content"></textarea>
</section>
  1. Luego modificaremos la propiedad computada notePreview para que ahora haga uso de selectedNote y retorne la vista previa de la nota que esté seleccionada:
computed: {
    notePreview () {
        return this.selectedNote ? marked(this.selectedNote.content) : ''
    },
},

Cambiando dinámicamente las clases CSS (v-bind)

Sería genial agregar dinámicamente una clase CSS al div de la nota actualmente seleccionada (por ejemplo, para modificar el color de fondo). Afortunadamente Vue nos permite hacer esto por medio del Enlace de clases y estilos.

Para hacer uso de esta característica en nuestra aplicación debemos agregar una directiva v-bind al elemento que corresponde a cada nota individual, de la siguiente forma:

...
<div 
    class="note" 
    v-for="note of notes"
    v-on:click="selectNote(note)"
    v-bind:class="{ selected: note === selectedNote }"
>{{ note.title }}</div>
...

Hasta este punto nuestra aplicación se ve así:

Markdown notebook with VueJS

Barra de comandos de funcionalidades de una nota

Aún nos falta por implementar dos funcionalidades más: renombrar una nota (cambiarle el título) y eliminar una nota. Proporcionaremos estas opciones al usuario por medio de una barra de comandos que agregaremos justo encima del elemento textarea. Para esto agregaremos un elemento div al cual le asignaremos la clase toolbar:

<section class="main">
    <div class="toolbar">
        <!-- Nuestra barra de comandos irá aquí -->
    </div>
    <textarea v-model="selectedNote.content"></textarea>
</section>

Renombrar una nota

Para implementar esta funcionalidad, utilizaremos un elemento input el cual crearemos dentro del elemento div que acabamos de crear. A este elemento input le asignaremos la respectiva directiva v-model para enlazarlo con la propiedad title de la nota actualmente seleccionada:

<section class="main">
    <div class="toolbar">
        <input 
            id="title" 
            v-model="selectedNote.title" 
            placeholder="Título de la nota" />
    </div>
    <textarea v-model="selectedNote.content"></textarea>
</section>

Eliminar una nota

Para implementar esta funcionalidad necesitamos crear un nuevo método que llamaremos removeNote.

  1. Añadiremos un elemento button junto al input recién creado:
<button @click="removeNote" title="Eliminar nota">
    <i class="material-icons">delete</i>
</button>

Como puedes ver, estamos escuchando el evento click con la directiva v-on la cual hemos asociado a un método llamado removeNote que crearemos en este momento. También hemos establecido un icono apropiado para el contenido del botón.

  1. Creamos el método removeNote el cual removerá la nota actualmente seleccionada del array de notas usando el método estandar de arrays en javascript: splice:
methods: {
    removeNote () {
        if (this.selectedNote) {
            const index = this.notes.indexOf(this.selectedNote);

            if (index !== -1) {
                this.notes.splice(index, 1)
            }
        }
    }
}

Renderizado condicional (v-if)

Antes de finalizar y revisar el resultado del trabajo realizado, falta implementar que tanto el panel principal como el panel de vista previa no se rendericen cuando no exista ninguna nota seleccionada. La directiva v-if nos permite mostrar o no elementos del DOM dependiendo de una condición dada.

Agregaremos la directiva v-if="selectedNote" al panel principal y al panel de vista previa, los cuales no serán añadidos al DOM a menos de una nota esté actualmente seleccionada:

...
<section class="main" v-if="selectedNote">
    <!-- Contenido del panel principal -->
</section>

...

<aside class="preview" v-if="selectedNote" v-html="notePreview"></aside>

...

Como pudiste notar hemos generado un poco de repetición en nuestro código 😟
Vue nos ofrece una solución para esto también 🦸, podemos envolver ambos elementos en una etiqueta especial template, la cual podríamos decir que funciona como unas llaves {} en Javascript:

<template v-if="selectedNote">
    <section class="main">
        <!-- Contenido del panel principal -->
    </section>

    <aside class="preview" v-html="notePreview"></aside>
</template>
...

Nuestra aplicación finalmente luce así 🤘

Markdown notebook with VueJS

Conclusión

Hemos desarrollado una aplicación sencilla que nos permitió practicar el funcionamiento de los aspectos básicos de Vue como enlace bidireccional (v-model) en un elemento textarea, propiedades computadas, renderizado de listas con v-for, métodos, gestión de eventos con v-on, asignación dinámica de clases y estilos con v-bind, renderizado condicional de elementos del DOM con v-if.

Puedes ver el demo funcional de la aplicación en este enlace. También aquí te dejo el código fuente del proyecto para que puedas descargarlo y usarlo como base para seguir mejorando tus habilidades en este gran framework Javascript: Vue JS.

Autor

Hola 👋, Soy Alejandro, un desarrollador de software que disfruta crear y mejorar herramientas que resuelvan problemas y hagan que la vida de las personas sea más sencilla, bella y cómoda. Algunas veces escribo sobre las cosas que he aprendido con el tiempo. Espero que el contenido te sea de ayuda.