Creación de una app de notas Markdown con VueJS
February 3, 2021 Compartir
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.
Metas
La aplicación ofrecerá las siguientes funcionalidades:
- Permitir al usuario crear notas formateadas con el lenguaje de marcado: Markdown.
- El documento markdown resultante será previsualizado en tiempo real.
- El usuario podrá agregar tantas notas como desee.
- Renombrar una nota (modificar el titulo).
- Eliminar una nota.
Para esto la interfaz de nuestra aplicación estará dividida en tres secciones:
- Un panel izquierdo que contendrá el listado de todas las notas creadas y un botón para agregar nuevas notas.
- Un panel central que contendrá el editor de la nota actualmente seleccionada.
- Un panel a la derecha que mostrará la previsualización de la nota actualmente seleccionada.
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.
- 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.
- 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 esnotebookApp
:
new Vue({
el: "#notebookApp",
});
- Ahora, en nuestra instancia de Vue, crearemos un nuevo objeto
data
el cual tendrá una propiedad llamadacontent
, 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:
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:
id:
El identificador único de la notatitle:
El título o nombre de la notacontent:
El contenido en formato markdowncreated:
La fecha y hora de creació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:
Mostrando el listado de notas (v-for)
Mostaremos nuestro listado de notas justo debajo del botón recién creado.
- Añadimos un nuevo elemento
div
con la clase cssnotes
:
...
<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! 😍:
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).
- Agregaremos una nueva propiedad al objeto
data
que llamaremosselectedId
, la cual almacenará el ID de la nota actualmente seleccionada:
data () {
return {
content: '',
notes: [],
selectedId: null,
}
},
- 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
},
}
...
- 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 directivav-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.
- Añadiremos una nueva propiedad computada que llamaremos
selectedNote
, la cual retornará el objetonote
que corresponda al ID que coincida con la propiedadselectedId
:
computed: {
selectedNote () {
return this.notes.find(note => note.id === this.selectedId)
},
}
- Necesitamos reemplazar la propiedad
content
porselectedNote.content
:
<section class="main">
<textarea v-model="selectedNote.content"></textarea>
</section>
- Luego modificaremos la propiedad computada
notePreview
para que ahora haga uso deselectedNote
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í:
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
.
- Añadiremos un elemento
button
junto alinput
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.
- 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í 🤘
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.