Crear calendarios de eventos con JavaScript y FullCalendar

Aprende cómo crear calendarios de eventos con JavaScript y la librería FullCalendar, una herramienta para construir calendarios dinámicos.

En esta publicación te explico cómo crear calendarios programáticamente con JavaScript sin esfuerzo, gracias la biblioteca FullCalendar. Puedes ver el ejercicio terminado haciendo clic en la siguiente imagen

Ejercicio terminado para crear calendarios con JavaScript y la librería FullCalendar. Haz click en la imágen para abrirlo en una ventana nueva.
Ejercicio terminado para crear calendarios con JavaScript y la librería FullCalendar. Haz click en la imagen para abrirlo en una ventana nueva.

¿Eres de los que les gusta pasar a la acción? Sáltate la teoría y ve directamente al tutorial práctico.

Recientemente me tuve que enfrentar a la creación de un sistema de reservas online.

Su desarrollo ha sido todo un reto para mí, del cual he aprendido mucho. Especialmente en lo que a la implementación frontend se refiere.

Como ya supondrás, crear una aplicación de esas características requiere del uso de un componente web esencial. 

Me refiero, por supuesto, al calendario. No existe una forma mejor para agregar y visualizar eventos, que no sea mediante un calendario interactivo.

Crea calendarios con JavaScript
Crea calendarios con JavaScript

En este artículo te voy a resumir todo lo que aprendí integrando un calendario web a través de la librería JavaScript FullCalendar.

FullCalendar, la librería JavaScript para crear calendarios interactivos

FullCalendar es una herramienta JavaScript que permite generar calendarios dinámicos mediante código HTML5.

https://fullcalendar.io/

Como cabe esperar, con ella serás capaz de representar eventos correctamente agrupados por fecha y hora.

Pero además, tendrás la posibilidad de modificar la visualización, agregando los eventos por meses, semanas, e incluso en forma de agenda.

Con unas pocas líneas de código serás capaz de desarrollar programas tan espectaculares como el mismísimo Google Calendar.

¿Qué tan popular es FullCalendar? 

En el momento de redactar estas líneas, su repositorio Github ha acumulado más de 16k estrellas.

Se descarga una media de 1M de veces al mes, y el equipo detrás de su desarrollo, lo mantiene actualizado hasta la fecha.

Es importante no confundir FullCalendar con un simple “datepicker”. Tal como su nombre sugiere, una vez instanciado, el componente ocupará todo el espacio que pueda en pantalla para una mejor navegabilidad.

FullCalendar, también se integra bien en proyectos React, Vue y Angular mediante “wrappers”. Aunque en el tutorial que he preparado, usaré “vanilla” JavaScript.

Por cierto, si no sabes qué es un wrapper para React, o te gustaría aprender a hacer los tuyos propios para cualquier librería JS, te animo a leer el siguiente artículo. 

Integra cualquier librería JavaScript en React

De base, esta herramienta proporciona un sinfín de posibilidades, pero además permite agregar plugins (gratuitos y premium) que extienden sus capacidades.

No te preocupes, pronto llegaremos a eso.

Los métodos y propiedades de FullCalendar 

Ya te he descrito, a grandes rasgos, qué ofrece ésta biblioteca, pero ahora toca ver en detalle cómo lo hace.

El set de herramientas que constituye FullCalendar se encuentra dividido en varios módulos, siendo “core” el principal.

Mediante comandos NPM instalamos el “core» de la librería en nuestro proyecto, con la instrucción que sigue. 

npm install @fullcalendar/core

Posteriormente agregaremos los módulos que nos puedan interesar, para incrementar las capacidades de nuestro calendario.

npm install @fullcalendar/daygrid
npm install @fullcalendar/timegrid
npm install  @fullcalendar/list

En este enlace encontrarás un listado completo de todos los módulos disponibles:

https://fullcalendar.io/docs/plugin-index

Tras instalar la dependencia principal y las extensiones, importamos el servicio Calendar.

import { Calendar } from "@fullcalendar/core";

El constructor de la clase Calendar acepta dos parámetros.

El primero es una referencia al elemento del DOM, que actuará de contenedor para el calendario. Recuerda, que el tamaño definido por CSS del contenedor, será el tamaño final del calendario.

Obtener una referencia a cualquier elemento HTML de tu aplicación, es tan sencillo como lanzar el método «document.querySelector()» pasando un selector css.

El segundo parámetro es necesario para instanciar la clase Calendar, se trata de un objeto JavaScript literal.

Este objeto se compone de una serie de propiedades que sirven para configurar parámetros del calendario.

Seguidamente veremos un ejemplo de cómo instanciar la clase, y algunas de las opciones más significativas.

import { Calendar } from '@fullcalendar/core'
import dayGridPlugin from '@fullcalendar/daygrid'
const calendar = new Calendar(container, {
  plugins: [dayGridPlugin],
  initialView: "dayGridMonth",
  headerToolbar: {
    left: "prev,next today",
    center: "title",
    right: "dayGridMonth,timeGridWeek,listWeek",
  },
  selectable: true,
  events: [
    {id: 1, title: "Event 1", start:"2023-09-05", end:"2023-09-10"},
    {id: 2, title: "Event 2", start:"2023-09-12", end:"2023-09-14"}],
  eventClick: (eventInfo)=>{console.log('Click on Event')},
  select: (dateRange)=>{console.log("Selected date range in calendar")},
});
  • plugins: Acepta una array con los módulos adicionales que extienden las capacidades del calendario.
  • initialView: Con un texto indicamos a FullCalendar qué tipo de visualización deseamos de inicio.
  • locale: FullCalendar también da soporte a múltiples idiomas, puedes configurar el que quieras con la propiedad locale.
  • headerToolbar: De forma muy declarativa, se puede asignar qué botones deben aparecer en la parte superior del calendario.
  • events: Sin duda la propiedad más importante, en ella deberemos pasar un array de objetos que describen la información de cada evento. En seguida veremos su DTO.
  • eventClick: Mediante la asignación de una función de callback, se define qué acción se desencadena cuando un usuario hace “click” en un evento en particular.
  • select: Otro callback que se ejecuta cuando el usuario selecciona un rango de tiempo dentro del calendario. Especialmente útil a la hora de agregar nuevos eventos en un día determinado.

Por supuesto, existen muchas otras propiedades, te animo a que investigues tu mismo la documentación para tener una mayor compresión de las posibilidades.

Tal y como vimos, para alimentar el calendario de contenido, es preciso incluir la propiedad “events” con una serie de objetos.

Estos objetos “event” representan la información de un evento en el calendario.

La estructura de datos que deben respetar estos objetos es la siguiente:

  • id: identificador único de cada evento.
  • title: título del acontecimiento.
  • allDay: booleano opcional para determinar si el evento dura todo el día o no.
  • start: Fecha de inicio
  • end: Fecha final
  • extendedProps: objeto opcional totalmente versátil, para almacenar cualquier tipo de información adicional.

Como siempre, solo he destacado las propiedades que me han parecido más relevantes, tienes el listado completo en el siguiente enlace.

https://fullcalendar.io/docs/event-object

Crear un calendario desde cero con JavaScript, FullCalendar y LocalStorage

Vamos a poner en práctica todo lo aprendido con un ejercicio práctico.

En concreto, desarrollaremos un calendario capaz de almacenar eventos en la memoria del navegador.

El principal objetivo de esta aplicación será ofrecer al usuario una interfaz que le permita seleccionar un rango de tiempo en un calendario, y adjuntar una información a modo de recordatorio.

Idealmente deberíamos habilitar un backend conectado a una base de datos, y almacenar ahí toda la información. No obstante, para simplificar el ejercicio, usaremos la API de navegador LocalStorage, como motor de DB.

Como viene siendo habitual, dejo a tu disposición el código fuente del ejercicio terminado.

No dudes en acudir a él, si alguna parte del tutorial no está debidamente explicada.

https://github.com/Danivalldo/libreriasjs/tree/master/FullCalendar

Comenzamos, por supuesto, instalando las dependencias del proyecto.

npm i @fullcalendar/core @fullcalendar/daygrid @fullcalendar/interaction @fullcalendar/list @fullcalendar/timegrid @dile/dile-modal

Además de la librería FullCalendar, he instalado un componente web para la rápida implementación de una modal, llamado dile-modal.

Seguidamente generamos un layout HTML muy sencillo.

Estructuramos la interfaz en dos áreas bien definidas.

Por un lado declaramos una cabecera, un espacio para cargar el calendario y un pié de página.

<div class="main-container">
  <div class="header">
    <img src="./imgs/calendar.svg" class="logo" alt="" />
    <h1>Mi calendario</h1>
  </div>
  <div class="calendar-container"></div>
  <footer class="footer">
    <a href="https://libreriasjs.com">www.libreriasjs.com</a>
  </footer>
</div>

Por el otro, preparamos la modal que contiene un formulario para la creación, edición y eliminación de los eventos.

<dile-modal id="event-modal" class="modal-container">
  <h2>Gestiona tu evento</h2>
  <form>
    <input type="text" placeholder="Nombre del evento" name="title" />
    <textarea
      name="description"
      id=""
      cols="30"
      rows="8"
      placeholder="Descripción del evento"
    ></textarea>
    <div class="btns-container">
      <button type="submit">Guardar</button>
      <button type="button" class="delete-event-btn d-none">
        Eliminar
      </button>
    </div>
  </form>
</dile-modal>

Dotar de estilos la UI es clave para una buena experiencia de usuario, así que vamos a añadir un poco de CSS.

@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;700&display=swap");
:root {
  --mainColor: #5b48d9;
  --mainColorDarker: #3f3197;
  --mainColorLighter: #7e6deb;
  --dangerColor: #d53b3b;
  --dile-modal-border-radius: 5px;
  --dile-modal-content-shadow-displacement: 0px;
  --dile-modal-content-shadow-blur: 30px;
  --dile-modal-content-shadow-color: rgba(0, 0, 0, 0.2);
  --dile-modal-width: 500px;
  --dile-modal-content-padding: 20px;
  --fc-event-bg-color: var(--mainColor);
  --fc-event-border-color: var(--mainColorLighter);
}
body {
  padding: 0;
  margin: 0;
  font-family: "Open Sans", sans-serif;
  background-color: #f2f2f2;
  color: #191919;
}
.main-container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}
.main-container .header {
  color: #fff;
  background-color: var(--mainColor);
  padding: 20px 0;
}
.main-container .header .logo {
  display: block;
  width: 100%;
  max-width: 60px;
  margin: 0 auto;
}
.main-container .header h1 {
  margin: 0;
  font-weight: 100;
  font-size: 2rem;
  text-align: center;
}
.main-container .calendar-container {
  flex: 1;
  max-width: 1300px;
  margin: 0 auto;
  width: 100%;
  padding: 20px;
}
.main-container .footer {
  color: #fff;
  background-color: var(--mainColorDarker);
  padding: 20px;
  font-size: 0.9rem;
  text-align: center;
}
.main-container .footer a {
  color: #fff;
  text-decoration: none;
}
.modal-container h2 {
  color: var(--mainColor);
  text-align: center;
  font-size: 1rem;
  margin: 0 0 20px 0;
}
.modal-container input,
.modal-container textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
  font-family: "Open Sans", sans-serif;
  padding: 10px;
  margin-bottom: 10px;
}
.modal-container .btns-container {
  display: flex;
}
.modal-container button {
  display: block;
  padding: 15px 25px;
  cursor: pointer;
  font-size: 1rem;
  color: #fff;
  flex: 1;
  background-color: var(--mainColor);
  border: none;
  border-radius: 4px;
  margin: 5px;
}
.modal-container button.delete-event-btn {
  background-color: var(--dangerColor);
}
.d-none {
  display: none !important;
}
.fc .fc-toolbar-title {
  color: var(--mainColorDarker);
}
@media (max-width: 768px) {
  .fc .fc-toolbar {
    display: block;
    text-align: center;
  }
  .fc .fc-toolbar-title {
    margin: 10px 0;
  }
}

Puedes ajustar colores, tipografía y acabados según tus intereses.

E incluso te animo a que incluyas alguna imagen para darle un toque personal a tu programa.

Seguimos el desarrollo, programado la interacción con JavaScript y la librería FullCalendar.

Como bien sabes, se puede llegar a un mismo resultado de múltiples formas. En este caso he dividido el código en dos archivos.

El primero se encuentra en el directorio «./scripts” bajo el nombre de “EventManager.js». Se trata de una clase que actúa como servicio para mantener los eventos sincronizados con LocalStorage.

Vamos a analizar su código punto a punto.

class EventManager {
  constructor() {
    this.eventsKey = "ljs-events";
    const events = localStorage.getItem(this.eventsKey);
    if (!events) {
      localStorage.setItem(this.eventsKey, JSON.stringify([]));
    }
  }
}
export default EventManager;

En el constructor declaramos una propiedad “eventsKey”, usaremos esta “string” como nombre de referencia al ítem con el que trabajaremos más adelante.

Al instanciar la clase, tratamos de obtener el objeto del “local storage”, si este no existe almacenamos una variable vacía.

getEvents() {
  return JSON.parse(localStorage.getItem(this.eventsKey));
}

Tal como su nombre sugiere, este método obtiene y parsea el conjunto de eventos guardados en la memoria del navegador.

saveEvent(event) {
  const events = this.getEvents();
  localStorage.setItem(this.eventsKey, JSON.stringify([event, ...events]));
}

Por contra, este otro evento codifica y almacena nuevos eventos combinándolos con los ya existentes.

updateEvent(newEvent, id) {
  const events = this.getEvents();
  localStorage.setItem(
    this.eventsKey,
    JSON.stringify(
      events.map((event) =>
        event.id === id ? { ...event, ...newEvent } : event
      )
    )
  );
}

Esta función se encarga de actualizar parte del contenido de un evento ya almacenado, por ese motivo recibe como parámetros el id y la información actualizada.

deleteEvent(id) {
  const events = this.getEvents();
  localStorage.setItem(
    this.eventsKey,
    JSON.stringify(events.filter((event) => event.id !== id))
  );
}

Como ya imaginarás, a través de esta instrucción eliminaremos de la memoria un evento indicado a través de su id.

En definitiva esta clase actúa como un CRUD muy básico.

A continuación, vamos a analizar el código del script principal, llamado main.js.

import { Calendar } from "@fullcalendar/core";
import LocaleEs from "@fullcalendar/core/locales/es";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import EventManager from "./scripts/EventManager";
import listPlugin from "@fullcalendar/list";
import "@dile/dile-modal/dile-modal";
import "./style.css";

Las primeras instrucciones del programa importarán todas las dependencias. 

Tanto aquellas que se encuentran instaladas en el directorio “node_modules”, como las generadas por nosotros mismos.

//variables const y let

const container = document.querySelector(".calendar-container");
const eventModal = document.querySelector("#event-modal");
const form = eventModal.querySelector("form");
let selectedInfo = null;
let selectedEvent = null;

Declaramos una serie de variables:

  • container” es una referencia al elemento del DOM donde FullCalendar desplegará un el nuevo calendario.
  • eventModal” es una referencia al contenedor DOM de la modal de creación / edición de eventos.
  • form” guarda relación con la etiqueta <form> de nuestro index.html dentro de la modal.
  • Usaremos “selectedInfo” y “selectedEvent” para guardar información relativa a eventos creados o nueva información.
const eventManager = new EventManager();

Creamos un objeto nuevo de nuestro servicio EventManager, antes mencionado.

En las siguientes líneas definimos una serie de funciones que posteriormente anclamos al comportamiento del calendario.

const handleOnSelect = (info) => {
  console.log("selected " + info.startStr + " to " + info.endStr);
  selectedInfo = info;
  eventModal.open();
};

Mediante esta función mostramos la modal oculta al usuario, y actualizamos “selectedInfo”. Llamaremos a esta función al hacer click en un dia del calendario, pronto llegaremos a eso.

const handleOnClickEvent = (data) => {
  form.querySelector('[name="title"]').value = data.event.title;
  form.querySelector('[name="description"]').value =
    data.event.extendedProps.description;
  selectedEvent = data.event;
  eventModal.querySelector(".delete-event-btn").classList.remove("d-none");
  eventModal.querySelector("button[type='submit']").innerHTML = "Editar";
  eventModal.open();
};

HandleOnClickEvent se ejecutará al hacer click en un evento del calendario. Al ejecutarse rellenará los campos del formulario con los datos correspondientes.

const handleOnSubmitForm = (e) => {
  e.preventDefault();
  const title = e.target.querySelector('[name="title"]').value;
  const description = e.target.querySelector('[name="description"]').value;
  if (!title.trim() || !description.trim()) {
    return;
  }
  if (selectedEvent) {
    selectedEvent.setProp("title", title);
    selectedEvent.setExtendedProp("description", description);
    eventManager.updateEvent({ title, description }, selectedEvent.id);
  } else {
    const event = {
      id: `${Date.now()}`,
      title,
      extendedProps: {
        description,
      },
      start: selectedInfo.startStr,
      end: selectedInfo.endStr,
    };
    eventManager.saveEvent(event);
    calendar.addEvent(event);
  }
  eventModal.close();
};

Callback que asignaremos al envío de un formulario. La lógica descrita en su interior sigue el siguiente razonamiento:

  • Extrae la información del formulario y lo “limpia” mediante trim().
  • Comprueba si existe un evento seleccionado.
  • En caso afirmativo, edita su contenido con el método updateEvent de nuestro servicio.
  • En caso negativo, genera un evento de FullCalendar nuevo, lo almacena en la memoria local, y lo añade al calendario (pronto llegaremos al objeto “calendar”).
  • Cierra la modal.
eventModal.querySelector(".delete-event-btn").addEventListener("click", () => {
  eventManager.deleteEvent(selectedEvent.id);
  selectedEvent.remove();
  eventModal.close();
});
form.addEventListener("submit", handleOnSubmitForm);
eventModal.addEventListener("dile-modal-closed", () => {
  form.querySelector('[name="title"]').value = "";
  form.querySelector('[name="description"]').value = "";
  eventModal.querySelector(".delete-event-btn").classList.add("d-none");
  eventModal.querySelector("button[type='submit']").innerHTML = "Guardar";
  selectedInfo = null;
  selectedEvent = null;
});

Declaramos los “listeners” para las acciones de eliminar eventos, enviar formularios y cerrar la modal.

const calendar = new Calendar(container, {
  plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
  initialView: "dayGridMonth",
  longPressDelay: 100,
  locale: LocaleEs,
  headerToolbar: {
    left: "prev,next today",
    center: "title",
    right: "dayGridMonth,timeGridWeek,listWeek",
  },
  selectable: true,
  events: eventManager.getEvents(),
  eventClick: handleOnClickEvent,
  select: handleOnSelect,
});
calendar.render();

Y por último, pero no por ello menos importante, declaramos una nueva instancia de la librería FullCalendar, con todos los parámetros de configuración. Guardamos el objeto en una variable llamada calendar.

Más material para seguir aprendiendo.

Como suele ser habitual, con este pequeño proyecto, apenas hemos rascado la superficie de todo el potencial que ofrece ésta herramienta.

Sin embargo, espero que te haya resultado de ayuda en tu proyecto. Por supuesto, puedes dejar comentarios con dudas o sugerencias si lo deseas.

En caso de que te hayas quedad con ganas de más te animo a crear una aplicación, completamente de cero que contenga un calendario, y ya de paso, puedes agregar tests e2e durante su desarrollo.

Cómo crear tests e2e para aplicaciones reales

Para terminar, voy a incluir una serie de enlaces a material de ampliación y ayuda que puede ser de interés para tí.

Saludos y hasta la próxima.

4 comentarios en «Crear calendarios de eventos con JavaScript y FullCalendar»

  1. Debido a que cuento con poca experiencia en el campo de la programación, se me ha dificultado usar la librería FullCalendar.

    Pido una consultoría, asesoría o apoyo de tus servicios. Gracias.

    Responder
  2. Hola Dani: Desde Bogotá, capital de la República de Colombia en Suramérica, mil gracias por este tutorial. Muy completo, ordenado y sobre todo con la explicación tan minuciosa.

    Por la distancia no te puedo invitar a ese anhelado café, pero cuando estés compartiendo con tus amigos y colegas deléitate una taza en mi nombre.

    Hasta una próxima oportunidad!!!!!

    Responder

Deja un comentario