Hagamos un calendario mensual con tecnología Vue
¿Alguna vez ha visto un calendario en una página web y ha pensado: cómo diablos hicieron eso? Para algo así, podría ser natural recurrir a un complemento, o incluso a un Google Calendar integrado, pero en realidad es mucho más sencillo crear uno de lo que piensas. Especialmente cuando utilizamos el poder impulsado por componentes de Vue.
He configurado una demostración en CodeSandbox para que puedas ver lo que pretendemos, pero siempre es una buena idea explicar lo que intentamos hacer:
- Cree una cuadrícula de vista mensual que muestre los días del mes actual
- Muestra las fechas del mes anterior y siguiente para que la cuadrícula esté siempre llena
- Indica la fecha actual
- Mostrar el nombre del mes seleccionado actualmente
- Navegar al mes anterior y siguiente
- Permitir al usuario regresar al mes actual con un solo clic
Ah, y crearemos esto como una aplicación de una sola página que recupera las fechas del calendario de Day.js, una biblioteca de utilidades súper liviana.
Paso 1: comenzar con el marcado básico
Saltaremos directamente a las plantillas. Si eres nuevo en Vue, la serie de introducción de Sarah es un buen lugar para comenzar. También vale la pena señalar que vincularé los documentos de Vue 2 a lo largo de esta publicación. Vue 3 se encuentra actualmente en versión beta y sus documentos están sujetos a cambios.
Comenzamos a crear una plantilla básica para nuestro calendario. Podemos delinear nuestro marcado en tres capas donde tenemos:
- Una sección para el encabezado del calendario. Esto mostrará los componentes con el mes seleccionado actualmente y los elementos responsables de la paginación entre meses.
- Una sección para el encabezado de la cuadrícula del calendario. Encabezado de tabla que contiene una lista que contiene los días de la semana, comenzando por el lunes.
- La cuadrícula del calendario. Ya sabes, cada día del mes actual, representado como un cuadrado en la cuadrícula.
Escribamos esto en un archivo llamado CalendarMonth.vue
. Este será nuestro componente principal.
!-- CalendarMonth.vue --template !-- Parent container for the calendar month -- div !-- The calendar header -- div !-- Month name -- CalendarDateIndicator / !-- Pagination -- CalendarDateSelector / /div !-- Calendar grid header -- CalendarWeekdays / !-- Calendar grid -- ol CalendarMonthDayItem / /ol /div/template
Ahora que tenemos algunas marcas con las que trabajar, vayamos un paso más allá y creemos los componentes necesarios.
Paso 2: componentes del encabezado
En nuestro encabezado tenemos dos componentes:
CalendarDateIndicator
muestra el mes seleccionado actualmente.CalendarDateSelector
es responsable de paginar entre meses.
Empecemos con CalendarDateIndicator
. Este componente aceptará una selectedDate
propiedad que es un objeto Day.js que formateará la fecha actual correctamente y se la mostrará al usuario.
!-- CalendarDateIndicator.vue --template div{{ selectedMonth }}/div/templatescriptexport default { props: { selectedDate: { type: Object, required: true } }, computed: { selectedMonth() { return this.selectedDate.format("MMMM YYYY"); } }};/script
Eso fue fácil. Vamos a crear el componente de paginación que nos permite navegar entre meses. Contendrá tres elementos encargados de seleccionar el mes anterior, actual y siguiente. Agregaremos un detector de eventos en aquellos que activan el método apropiado cuando se hace clic en el elemento.
!-- CalendarDateSelector.vue --template div span @click="selectPrevious"﹤/span span @click="selectCurrent"Today/span span @click="selectNext"﹥/span /div/template
Luego, en la sección del script, configuraremos dos accesorios que el componente aceptará:
currentDate
nos permite volver al mes actual cuando se hace clic en el botón “Hoy”.selectedDate
Nos dice qué mes está seleccionado actualmente.
También definiremos métodos responsables de calcular la nueva fecha seleccionada en función de la fecha seleccionada actualmente utilizando los métodos subtract
y add
de Day.js. Cada método también envía $emit
un evento al componente principal con el mes recién seleccionado. Esto nos permite mantener el valor de la fecha seleccionada en un lugar (que será nuestro CalendarMonth.vue
componente) y pasarlo a todos los componentes secundarios (es decir, encabezado, cuadrícula de calendario).
// CalendarDateSelector.vuescriptimport dayjs from "dayjs";export default { name: "CalendarDateSelector", props: { currentDate: { type: String, required: true }, selectedDate: { type: Object, required: true } }, methods: { selectPrevious() { let newSelectedDate = dayjs(this.selectedDate).subtract(1, "month"); this.$emit("dateSelected", newSelectedDate); }, selectCurrent() { let newSelectedDate = dayjs(this.currentDate); this.$emit("dateSelected", newSelectedDate); }, selectNext() { let newSelectedDate = dayjs(this.selectedDate).add(1, "month"); this.$emit("dateSelected", newSelectedDate); } }};/script
Ahora, volvamos al CalendarMonth.vue
componente y usamos nuestros componentes recién creados.
Para usarlos primero necesitamos importar y registrar los componentes, también necesitamos crear los valores que pasarán como accesorios a esos componentes:
today
formatea correctamente la fecha de hoy y se utiliza como valor para el botón de paginación “Hoy”.selectedDate
es la fecha seleccionada actualmente (establecida en la fecha de hoy de forma predeterminada).
Lo último que debemos hacer antes de poder renderizar los componentes es crear un método que sea responsable de cambiar el valor de selectedDate
. Este método se activará cuando se reciba el evento del componente de paginación.
// CalendarMonth.vuescriptimport dayjs from "dayjs";import CalendarDateIndicator from "./CalendarDateIndicator";import CalendarDateSelector from "./CalendarDateSelector";export default { components: { CalendarDateIndicator, CalendarDateSelector }, data() { return { selectedDate: dayjs(), today: dayjs().format("YYYY-MM-DD") }; }, methods: { selectDate(newSelectedDate) { this.selectedDate = newSelectedDate; } }};/script
Ahora tenemos todo lo que necesitamos para representar el encabezado de nuestro calendario:
!-- CalendarMonth.vue --template div div CalendarDateIndicator :selected-date="selectedDate" / CalendarDateSelector :current-date="today" :selected-date="selectedDate" @dateSelected="selectDate" / /div /div/template
Éste es un buen lugar para detenerse y ver lo que tenemos hasta ahora. Nuestro encabezado de calendario hace todo lo que queremos, así que avanzamos y creemos componentes para nuestra cuadrícula de calendario.
Paso 3: componentes de la cuadrícula del calendario
Aquí, nuevamente, tenemos dos componentes:
CalendarWeekdays
muestra los nombres de los días de la semana.CalendarMonthDayItem
representa un solo día en el calendario.
El CalendarWeekdays
componente contiene una lista que registra en iteración las etiquetas de los días de la semana (usando la v-for
directiva) y representa esa etiqueta para cada día de la semana. En la sección del script, debemos definir nuestros días de la semana y crear una computed
propiedad para que esté disponible en la plantilla y almacenar en caché el resultado para evitar que tengamos que volver a calcularlo en el futuro.
// CalendarWeekdays.vuetemplate ol li v-for="weekday in weekdays" :key="weekday" {{ weekday }} /li /ol/template
scriptconst WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];export default { name: 'CalendarWeekdays', computed: { weekdays() { return WEEKDAYS } }}/script
El siguiente es CalendarMonthDayItem
. Es un elemento de lista que recibe una day
propiedad que es un objeto y una propiedad booleana, isToday
que nos permite diseñar el elemento de la lista para indicar que es la fecha actual. También tenemos una computed
propiedad que formatea el objeto del día recibido en nuestro formato de fecha deseada ( D
o el día numérico del mes).
// CalendarMonthDayItem.vuetemplate li :class="{ 'calendar-day--not-current': !isCurrentMonth, 'calendar-day--today': isToday }" span{{ label }}/span /li/template
scriptimport dayjs from "dayjs";export default { name: "CalendarMonthDayItem", props: { day: { type: Object, required: true }, isCurrentMonth: { type: Boolean, default: false }, isToday: { type: Boolean, default: false } }, computed: { label() { return dayjs(this.day.date).format("D"); } }};/script
Bien, ahora que tenemos estos dos componentes, veamos cómo podemos agregarlos a nuestro CalendarMonth
componente.
Primero necesitamos importarlos y registrarlos. También necesitamos crear una computed
propiedad que devolverá una serie de objetos que representan nuestros días. Cada día contiene una date
propiedad y isCurrentMonth
una propiedad.
// CalendarMonth.vuescriptimport dayjs from "dayjs";import CalendarMonthDayItem from "./CalendarMonthDayItem";import CalendarWeekdays from "./CalendarWeekdays";
export default { name: "CalendarMonth", components: { // ... CalendarMonthDayItem, CalendarWeekdays }, computed: { days() { return [ { date: "2020-06-29", isCurrentMonth: false }, { date: "2020-06-30", isCurrentMonth: false }, { date: "2020-07-01", isCurrentMonth: true }, { date: "2020-07-02", isCurrentMonth: true }, // ... { date: "2020-07-31", isCurrentMonth: true }, { date: "2020-08-01", isCurrentMonth: false }, { date: "2020-08-02", isCurrentMonth: false } ]; } }};/script
Luego, en la plantilla, podemos renderizar nuestros componentes. Nuevamente, usamos la v-for
directiva para representar la cantidad requerida de elementos del día.
!-- CalendarMonth.vue --template div div // ... /div CalendarWeekdays/ ol CalendarMonthDayItem v-for="day in days" :key="day.date" :day="day" :is-today="day.date === today" / /ol /div/template
Bien, las cosas empiezan un verso bien ahora. Eche un vistazo a dónde estamos. Se ve bien pero, como probablemente habrás notado, la plantilla solo contiene datos estáticos por el momento. El mes está codificado como julio y los números de los días también están codificados. Cambiaremos eso calculando qué fecha debe mostrarse en un mes específico. ¡Vamos a sumergirnos en el código!
Paso 4: configurar el calendario del mes actual
Pensemos cómo podemos calcular la fecha que debe mostrarse en un mes específico. Ahí es donde realmente entra en el juego Day.js. Proporciona todos los datos que necesitamos para colocar correctamente las fechas en los días correctos de la semana para un mes determinado utilizando datos reales del calendario. Nos permite obtener y configurar cualquier cosa, desde la fecha de inicio de un mes hasta todas las opciones de formato de fecha que necesitamos para mostrar los datos.
Lo haremos:
- Obtener el mes actual
- Calcular dónde se deben colocar los días (días laborables)
- Calcular los días para mostrar las fechas del mes anterior y siguiente.
- Pon todos los días juntos en una sola matriz.
Ya tenemos Day.js importado en nuestro CalendarMonth
componente. También nos apoyaremos en un par de complementos de Day.js para obtener ayuda. WeekDay nos ayuda a establecer el primer día de la semana. Algunos prefieren el domingo como primer día de la semana. Otros prefieren el lunes. Diablos, en algunos casos, tiene sentido comenzar el viernes. Vamos a empezar el lunes.
El complemento WeekOfYear devuelve el valor numérico de la semana actual entre todas las semanas del año. Hay 52 semanas en un año, por lo que diríamos que la semana que comienza el 1 de enero es la primera semana del año, y así sucesivamente.
Esto es lo que ponemos CalendarMonth.vue
para poner todo eso en uso:
// CalendarMonth.vuescriptimport dayjs from "dayjs";import weekday from "dayjs/plugin/weekday";import weekOfYear from "dayjs/plugin/weekOfYear";// ...
dayjs.extend(weekday);dayjs.extend(weekOfYear);// ...
Eso fue bastante sencillo, pero ahora comienza la verdadera diversión, ya que jugaremos con la cuadrícula del calendario. Detengámonos por un segundo y pensemos qué es lo que realmente debemos hacer para hacerlo bien.
Primero, queremos que los números de fecha caigan en las columnas correctas de los días de la semana. Por ejemplo, el 1 de julio de 2020 es miércoles. Ahí es donde debería comenzar la numeración de fechas.
Si el primero del mes cae en miércoles, eso significa que tendremos elementos de cuadrícula vacíos para el lunes y martes de la primera semana. El último día del mes es el 31 de julio, que cae en viernes. Eso significa que el sábado y el domingo estarán vacíos en la última semana de la parrilla. Queremos llenarlos con las fechas iniciales y finales del mes anterior y siguiente, respectivamente, para que la cuadrícula del calendario esté siempre llena.
Agregar fechas para el mes actual
Para agregar los días del mes actual a la cuadrícula, necesitamos saber cuántos días existen en el mes actual. Podemos conseguirlo utilizando el daysInMonth
método proporcionado por Day.js. Creemos una computed
propiedad para eso.
// CalendarMonth.vuecomputed: { // ... numberOfDaysInMonth() { return dayjs(this.selectedDate).daysInMonth(); }}
Cuando sabemos eso, creamos una matriz vacía con una longitud igual a la cantidad de días del mes actual. Luego hacemos map()
esa matriz y creamos un objeto de día para cada uno. El objeto que creamos tiene una estructura arbitraria, por lo que puedes agregar otras propiedades si las necesitas.
Sin embargo, en este ejemplo necesitamos una date
propiedad que se usará para verificar si una fecha en particular es el día actual. También devolveremos un isCurrentMonth
valor que verifica si la fecha está en el mes actual o fuera de él. Si está fuera del mes actual, les aplicaremos un estilo para que la gente sepa que están fuera del rango del mes actual.
// CalendarMonth.vuecomputed: { // ... currentMonthDays() { return [...Array(this.numberOfDaysInMonth)].map((day, index) = { return { date: dayjs(`${this.year}-${this.month}-${index + 1}`).format("YYYY-MM-DD") isCurrentMonth: true }; }); },}
Agregar fechas del mes anterior
Para que las fechas del mes anterior se muestren en el mes actual, debemos verificar cuál es el día de la semana del primer día en el mes seleccionado. Ahí es donde podemos usar el complemento WeekDay para Day.js. Creemos un método auxiliar para eso.
// CalendarMonth.vuemethods: { // ... getWeekday(date) { return dayjs(date).weekday(); },}
Luego, en base a eso, debemos verificar qué día fue el último lunes del mes anterior. Necesitamos ese valor para saber cuántos días del mes anterior deben ser visibles en la vista del mes actual. Podemos obtenerlo restando el valor del día de la semana del primer día del mes actual. Por ejemplo, si el primer día del mes es miércoles, debemos restar tres días para obtener el último lunes del mes anterior. Tener ese valor nos permite crear una serie de objetos de día desde el último lunes del mes anterior hasta el final de ese mes.
// CalendarMonth.vuecomputed: { // ... previousMonthDays() { const firstDayOfTheMonthWeekday = this.getWeekday(this.currentMonthDays[0].date); const previousMonth = dayjs(`${this.year}-${this.month}-01`).subtract(1, "month"); // Cover first day of the month being sunday (firstDayOfTheMonthWeekday === 0) const visibleNumberOfDaysFromPreviousMonth = firstDayOfTheMonthWeekday ? firstDayOfTheMonthWeekday - 1 : 6; const previousMonthLastMondayDayOfMonth = dayjs(this.currentMonthDays[0].date).subtract(visibleNumberOfDaysFromPreviousMonth, "day").date(); return [...Array(visibleNumberOfDaysFromPreviousMonth)].map((day, index) = { return { date: dayjs(`${previousMonth.year()}-${previousMonth.month() + 1}-${previousMonthLastMondayDayOfMonth + index}`).format("YYYY-MM-DD"), isCurrentMonth: false }; }); }}
Agregar fechas del próximo mes
Ahora, hagamos lo contrario y calculemos qué días necesitamos del próximo mes para completar la cuadrícula del mes actual. Afortunadamente, podemos utilizar el mismo asistente que acabamos de crear para el cálculo del mes anterior. La diferencia es que calcularemos cuántos días del próximo mes deberían ser visibles restando el valor numérico del día de la semana de siete.
Entonces, por ejemplo, si el último día del mes es sábado, necesitamos restar un día de siete para construir una serie de fechas necesarias del próximo mes (domingo).
// CalendarMonth.vuecomputed: { // ... nextMonthDays() { const lastDayOfTheMonthWeekday = this.getWeekday(`${this.year}-${this.month}-${this.currentMonthDays.length}`); const nextMonth = dayjs(`${this.year}-${this.month}-01`).add(1, "month"); const visibleNumberOfDaysFromNextMonth = lastDayOfTheMonthWeekday ? 7 - lastDayOfTheMonthWeekday : lastDayOfTheMonthWeekday; return [...Array(visibleNumberOfDaysFromNextMonth)].map((day, index) = { return { date: dayjs(`${nextMonth.year()}-${nextMonth.month() + 1}-${index + 1}`).format("YYYY-MM-DD"), isCurrentMonth: false }; }); }}
Bien, sabemos cómo crear todos los días que necesitamos, así que usémoslos y fusionemos todos los días en una única matriz de todos los días que queremos mostrar en el mes actual, incluidas las fechas de relleno del mes anterior y siguiente.
// CalendarMonth.vuecomputed: { // ... days() { return [ ...this.previousMonthDays, ...this.currentMonthDays, ...this.nextMonthDays ]; },}
¡Voilá, ahí lo tenemos! Echa un vistazo a la demostración final para ver todo junto.
Deja un comentario