Hagamos un calendario mensual con tecnología Vue | Programar Plus

¿Alguna vez has visto un calendario en una página web y has pensado, cómo diablos hicieron eso? Para algo como eso, puede ser natural buscar un complemento, o incluso un Calendario de Google integrado, pero en realidad es mucho más sencillo hacer uno de lo que piensas. Especialmente cuando usamos el poder basado en componentes de Vue.

He configurado una demostración en CodeSandbox para que pueda ver lo que buscamos, pero siempre es una buena idea explicar en detalle lo que estamos tratando de hacer:

  • Cree una cuadrícula de vista de mes que muestre los días del mes actual
  • Muestra las fechas de los meses anteriores y siguientes para que la grilla esté siempre llena
  • Indicar la fecha actual
  • Mostrar el nombre del mes actualmente seleccionado
  • Navegar al mes anterior y siguiente
  • Permita que el usuario navegue de regreso al mes actual con un solo clic

Ah, y construiremos esto como una aplicación de una sola página que obtiene fechas de calendario de Day.js, una biblioteca de utilidades súper liviana.

Paso 1: Comience con el marcado básico

Vamos a saltar 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 estaré enlazando a los documentos de Vue 2 a lo largo de esta publicación. Vue 3 se encuentra actualmente en versión beta y los documentos están sujetos a cambios.

Comencemos con la creación de una plantilla básica para nuestro calendario. Podemos delinear nuestro marcado como tres capas donde tenemos:

  • Una sección para el encabezado del calendario. Esto mostrará componentes con el mes actualmente seleccionado y los elementos responsables de paginar entre meses.
  • Una sección para el encabezado de la cuadrícula del calendario. Un encabezado de tabla que contiene una lista que contiene los días de la semana, comenzando con 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 class="calendar-month">
     
    <!-- The calendar header -->
    <div class="calendar-month-header"
      <!-- Month name -->
      <CalendarDateIndicator />
      <!-- Pagination -->
      <CalendarDateSelector />
    </div>

    <!-- Calendar grid header -->
    <CalendarWeekdays />

    <!-- Calendar grid -->
    <ol class="days-grid">
      <CalendarMonthDayItem />
    </ol>
  </div>
</template>

Ahora que tenemos algunas marcas con las que trabajar, avancemos un paso más y creemos los componentes necesarios.

Paso 2: componentes del encabezado

En nuestra cabecera tenemos dos componentes:

  • CalendarDateIndicator muestra el mes actualmente seleccionado.
  • CalendarDateSelector se encarga de paginar entre meses.

Empecemos con CalendarDateIndicator. Este componente aceptará un selectedDate propiedad que es un objeto Day.js que formateará la fecha actual correctamente y se la mostrará al usuario.

<!-- CalendarDateIndicator.vue -->
<template>
  <div class="calendar-date-indicator">{{ selectedMonth }}</div>
</template>

<script>
export default {
  props: {
    selectedDate: {
      type: Object,
      required: true
    }
  },

  computed: {
    selectedMonth() {
      return this.selectedDate.format("MMMM YYYY");
    }
  }
};
</script>

Eso fue fácil. Vayamos y creemos el componente de paginación que nos permite navegar entre meses. Contendrá tres elementos encargados de seleccionar el mes anterior, actual y próximo. Agregaremos un detector de eventos en aquellos que activan el método apropiado cuando se hace clic en el elemento.

<!-- CalendarDateSelector.vue -->
<template>
  <div class="calendar-date-selector">
    <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 aceptará el componente:

  • 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 el subtract y add métodos de Day.js. Cada método también $emit un evento al componente principal con el mes recién seleccionado. Esto nos permite mantener el valor de la fecha seleccionada en un solo lugar, que será nuestro CalendarMonth.vue componente — y pasarlo a todos los componentes secundarios (es decir, encabezado, cuadrícula de calendario).

// CalendarDateSelector.vue
<script>
import 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 a la CalendarMonth.vue componente y utilizar nuestros componentes recién creados.

Para usarlos, primero debemos importar y registrar los componentes, también debemos crear los valores que se pasarán como accesorios a esos componentes:

  • todayformatea correctamente la fecha de hoy y se usa como un valor para el botón de paginación “Hoy”.
  • selectedDatees la fecha seleccionada actualmente (establecida en la fecha de hoy de forma predeterminada).

Lo último que debemos hacer antes de que podamos 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.vue
<script>
import 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 class="calendar-month">
    <div class="calendar-month-header">
      <CalendarDateIndicator
        :selected-date="selectedDate"
        class="calendar-month-header-selected-month"
      />
      <CalendarDateSelector
        :current-date="today"
        :selected-date="selectedDate"
        @dateSelected="selectDate"
      />
    </div>
  </div>
</template>

Este es un buen lugar para detenerse y ver lo que tenemos hasta ahora. Nuestro encabezado de calendario está haciendo todo lo que queremos, así que avancemos y creemos componentes para nuestra cuadrícula de calendario.

Paso 3: componentes de la cuadrícula del calendario

Aquí, de nuevo, 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 El componente contiene una lista que itera a través de las etiquetas de los días de la semana (usando el v-for directiva) y representa esa etiqueta para cada día de la semana. En la sección de script, necesitamos definir nuestros días de la semana y crear un computed property para que esté disponible en la plantilla y almacene en caché el resultado para evitar que tengamos que volver a calcularlo en el futuro.

// CalendarWeekdays.vue
<template>
  <ol class="day-of-week">
    <li
      v-for="weekday in weekdays"
      :key="weekday"
    >
      {{ weekday }}
    </li>
  </ol>
</template>


<script>
const 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 la lista que recibe un day propiedad que es un objeto, y un accesorio booleano, isToday, que nos permite diseñar el elemento de la lista para indicar que es la fecha actual. también tenemos uno computed propiedad que formatea el objeto de día recibido a nuestro formato de fecha deseado (D, o el día numérico del mes).

// CalendarMonthDayItem.vue
<template>
  <li
    class="calendar-day"
    :class="{
      'calendar-day--not-current': !isCurrentMonth,
      'calendar-day--today': isToday
    }"
  >
    <span>{{ label }}</span>
  </li>
</template>


<script>
import 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 tenemos que importarlos y registrarlos. También tenemos que crear un computed propiedad que devolverá una matriz de objetos que representan nuestros días. Cada día contiene un date propiedad y isCurrentMonth propiedad.

// CalendarMonth.vue
<script>
import 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 el v-for directiva para representar el número requerido de elementos de día.

<!-- CalendarMonth.vue -->
<template>
  <div class="calendar-month">
    <div class="calendar-month-header">
      // ...
    </div>

    <CalendarWeekdays/>

    <ol class="days-grid">
      <CalendarMonthDayItem
        v-for="day in days"
        :key="day.date"
        :day="day"
        :is-today="day.date === today"
      />
    </ol>
  </div>
</template>

OK, las cosas empiezan a verse bien ahora. Echa un vistazo a dónde estamos. Se ve bien pero, como probablemente hayas notado, la plantilla solo contiene datos estáticos en este 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: Configuración del calendario del mes actual

Pensemos cómo podemos calcular la fecha que debe mostrarse en un mes específico. Ahí es donde Day.js realmente entra en juego. 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 deben colocarse los días (días laborables)
  • Calcule los días para mostrar las fechas de los meses anterior y siguiente
  • Ponga todos los días juntos en una sola matriz

Ya tenemos Day.js importado en nuestro CalendarMonth componente. También vamos a apoyarnos 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 con el viernes. Vamos a empezar con el lunes.

El complemento WeekOfYear devuelve el valor numérico de la semana actual de 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 en CalendarMonth.vue para poner todo eso en uso:

// CalendarMonth.vue
<script>
import 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 ahora jugaremos con la cuadrícula del calendario. Detengámonos por un segundo a pensar en lo que realmente debemos hacer para hacerlo bien.

Primero, queremos que los números de fecha caigan en las columnas correctas del día de la semana. Por ejemplo, el 1 de julio de 2020 es miércoles. Ahí es donde debe comenzar la numeración de la fecha.

Si el primer día del mes cae en miércoles, eso significa que tendremos elementos de cuadrícula vacíos para el lunes y el martes en 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 anteriores y posteriores de los meses anterior y siguiente, respectivamente, para que la cuadrícula del calendario esté siempre llena.

La cuadrícula del calendario que muestra los dos primeros y los dos últimos días resaltados en rojo indica que provienen del mes anterior y del siguiente.

Adición de fechas para el mes actual

Para agregar los días del mes actual a la cuadrícula, necesitamos saber cuántos días hay en el mes actual. Podemos obtener eso usando el daysInMonth método proporcionado por Day.js. Vamos a crear un computed propiedad para eso.

// CalendarMonth.vue
computed: {
  // ...
  numberOfDaysInMonth() {
      return dayjs(this.selectedDate).daysInMonth();
  }
}

Cuando sabemos eso, creamos una matriz vacía con una longitud que es igual a la cantidad de días en el mes actual. Entonces nosotros map() esa matriz y cree un objeto de día para cada uno. El objeto que creamos tiene una estructura arbitraria, por lo que puede agregar otras propiedades si las necesita.

En este ejemplo, sin embargo, necesitamos un date propiedad que se utilizará para verificar si una fecha en particular es el día actual. También devolveremos un isCurrentMonth valor que comprueba si la fecha está en el mes actual o fuera de él. Si está fuera del mes actual, los diseñaremos para que la gente sepa que están fuera del rango del mes actual.

// CalendarMonth.vue
computed: {
  // ...
  currentMonthDays() {
    return [...Array(this.numberOfDaysInMonth)].map((day, index) => {
      return {
        date: dayjs(`${this.year}-${this.month}-${index + 1}`).format("YYYY-MM-DD")
        isCurrentMonth: true
      };
    });
  },
}

Adición de 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. Vamos a crear un método auxiliar para eso.

// CalendarMonth.vue
methods: {
  // ...
  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 obtener eso 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 matriz de objetos de día a partir del último lunes del mes anterior hasta el final de ese mes.

// CalendarMonth.vue
computed: {
  // ...
  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
      };
    });
  }
}

Adición de 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 usar el mismo ayudante 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, debemos restar un día de siete para construir una matriz de fechas necesarias a partir del próximo mes (domingo).

// CalendarMonth.vue
computed: {
  // ...
  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 sola matriz de todos los días que queremos mostrar en el mes actual, incluidas las fechas de relleno del mes anterior y del siguiente.

// CalendarMonth.vue
computed: {
  // ...
  days() {
    return [
      ...this.previousMonthDays,
      ...this.currentMonthDays,
      ...this.nextMonthDays
    ];
  },
}

¡Voila, ahí lo tenemos! Echa un vistazo a la demostración final para ver todo junto.

(Visited 32 times, 1 visits today)