Создание чертовски простого макета календаря с помощью CSS Grid, Moment.js и Vue.js

Перевод статьи Building a Damn Simple Calendar Layout with CSS Grid, Moment, and Vue

Пару дней назад, клиент прислал мне фотографию макета очень просто календаря в качестве вдохновения для компонента, который мы пытались добавить на разрабатываемый тогда сайт. Я понял его логику. Мы работали над сайтом, в котором использовалась очень строгая типографика и макет этого календаря идеально соответствовал его эстетике. Обычное статическое размещение чего-то подобного с помощью сетки довольно тривиально, но мы хотели убедиться, что сможем создать такой календарь динамически и тогда он сможет превратиться в компонент выбора даты. По ссылке вы можете открыть демо на Codepen и продолжить чтение статьи.

Прежде всего убедитесь, что у вас есть корневой элемент, в котором вы будете инициализировать ваше Vue приложение. Для меня это стал <main> c идентификатором “calendar”. Добавьте скрипт Vue.js в проект и инициализируйте его.

const app = new Vue({
el: '#calendar',
data() {
return {
days: []
}
},
mounted() {
// загружаем список дней
}
})

Отлично. Теперь добавляем moment.js и пишем в хук mounted() код который заполнит массив days днями месяца. Для этого будем использовать функции из moment.js и немного магии ES6.

let monthDate = moment().startOf('month')this.days = [...Array(monthDate.daysInMonth())].map((_, i) => {
return monthDate.clone().add(i, 'day')
})

Во-первых, получаем начало месяца с помощью функции startOf(). Во-вторых мы будем использовать функцию daysInMonth() как часть нового временного массива который будет распределен в массив, равный длине дней в месяце. И затем мы можем использовать его что бы пройтись по списку дней c помощью функций moment.js clone и add. Это добавит дни месяца в наш итоговый массив days.

Теперь, когда у нас есть массив дней месяца мы можем сделать html каркас что бы отобразить их. Для это будем использовать стандартный способ отрисовки списков в Vue.js. Добавьте этот код в основной контейнер вашего календаря:

<div v-for="(day, index) in days">
{{ day.format('D') }}
</div>

Теперь вы должны увидеть список дней, равный количеству дней в вашем текущем месяце. Теперь добавим немного CSS что бы календарь выглядел приятнее. Я хочу что бы он был центрирован и в нем использовался шрифт без засечек.

html, body{
height: 100%;
width: 100%;
}
body{
align-items: center;
box-sizing: border-box;
display: flex;
font: calc(2vh) "Helvetica", sans-serif;
justify-content: "center";
padding: 2em;
}

Так же настроим сетку календаря так, что бы она состояла из 7 колонок, но по ширине оставалась в разумных пределах на больших экранах. Давайте сделаем так, что бы цифры в каждой ячейке были в центре. И используем псевдоэлемент ::before что бы наши ячейки оставались квадратными.

#calendar{
display: grid;
grid-template-columns: repeat(7, 1fr);
max-width: 1024px;
width: 100%;
}
#calendar > *{
align-items: center;
display: flex;
justify-content: center;
}
#calendar > *::before{
content: "";
display: inline-block;
height: 0;
padding-bottom: 100%;
width: 1px;
}

Теперь вы увидите отличную сетку с днями, но месяц может начинаться не с той колонки. Это связано с тем, что нам нужно указать, что первый день месяца должен начинаться с определенного столбца, а остальные дни будут отображаться соответствующим образом. Вы можете сделать это, просто добавив свойство gridColumn с необходимым индексом к первому элементу div. Для этого напрямую через Vue.js укажем необходимые нам стили.

<div v-for="(day, index) in days" :style="{ gridColumn: column(index) }">
{{ day.format('D') }}
</div>

Вызываем метод column и в него передаем index текущего элемента. Это позволит проверить нам что это первый элемент, и если это так то мы используем moment.js что бы получить первый день недели (0–6) плюс один. Это как раз подходит для сетки нашего календаря. Добавьте этот метод в methods экземпляра Vue.

column(index) {
if (index == 0) {
return this.days[0].day() + 1
}
}

Было бы неплохо указывать текущий день месяца. Для этого в нашем списке укажем нужный класс опять же с помощью Vue.

<div v-for="(day, index) in days" :style="{ gridColumn: column(index) }" :class="{ today: today(day) }">
{{ day.format('D') }}
</div>

Если метод today возвратит true то у элемента появится класс today. А для этого с помощью функции moment.js inSame() мы проверим, совпадает ли день в списке с текущим днем. Так же добавляем метод today в methods экземпляра Vue.

today(day) {
return moment().isSame(day, 'day')
}

Вы можете добавить немного стилей что бы выделить текущий день, я например добавил рамку и скруглил её.

#calendar > *.today{
border: 0.1em solid black;
border-radius: 100%;
}

Ну и наконец добавьте вручную 7 блоков для обозначения дней недели и вы увидите как ваш календарь сам раскладывает дни месяца автоматически.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store