[Из песочницы] Верстаем flex-календарик
Идет 2018 год, модные пацаны давно уже верстают на grid, а я все на третьем бутстрапе сижу с col-md кочерячусь, мельком поглядывая на четвертый.
Решил я, что это не дело, и стоит немного знания освежить, но у grid вроде как поддержка пока хромает, а вот flex технологию уже даже утюги поддерживают.
Вот и решил его освоить. И процессом усвоения с вами поделится. В общем, будем верстать календарик на весь год.
Нам потребуется
- vue
- клей moment
- и чуток flex
Результат будет выглядеть примерно вот так:
Так получилось, что я адепт vue, и поэтому для рендера буду использовать его. Для работы с датами буду использовать библиотечку moment, можно и без нее обойтись, но мне с ней привычнее.
Подготовка
Устанавливаем vue-cli, если у вас её еще нет:
npm install -g vue-cli
создаем проект на базе шаблона webpack-simple, я буду использовать scss (в основном для комментов), поэтому когда визард спросит вас
? Use sass? (y/N)
ответьте y (es), в общем запускаем:
vue-init webpack-simple calendar_flex
cd calendar_flex
npm install
добавим библиотечку moment.js
npm install -S moment
очищаем файлик App.vue
Чтоб не показаться варваром, не будем просто верстать голый календарь, а разработаем отдельную компоненту.
Создадим файлик Calendar.vue:
Календарь
подключим компоненту глобально в main.js
import Vue from 'vue'
import App from './App.vue'
import Calendar from './Calendar.vue'
Vue.component("calendar", Calendar);
new Vue({
el: '#app',
render: h => h(App)
})
добавим компоненту в App.vue
...
Если все верно сделали, то увидим слово «Календарь» на белом фоне.
Готовим данные
Прежде чем что-то рисовать надо подготовить данные для календаря. Я предлагаю упихать данные по году в массив из месяцев. В свою очередь, каждый месяц будет представлять собой объект вида:
{
title: 'Январь',
weeks: {1: {}, 2: {}, ...}
}
то бишь название месяца и массив из недель. Каждая неделя будет представлять собой объект где к каждому дню (от 1 до 7) будет привязана дата и может еще какая-нибудь мета информация:
week = {
1: {date: new Date(), ...}, // понедельник
2: {date: new Date(), ...}, // вторник
...
}
переключимся на файлик Calendar.vue, и обновим часть ответственную за скрипт:
import moment from 'moment';
export default {
...
computed: {
yearData() {
let data = [];
for (let m = 0; m < 12; ++m) {
// формируем дату на первый день каждого месяца
let day = moment({year: this.year, month: m, day: 1});
let daysInMonth = day.daysInMonth(); // количество дней в месяце
let month = { // готовим объект месяца
title: day.format("MMMM"),
weeks: {},
};
// итерируем по количеству дней в месяце
for (let d = 0; d < daysInMonth; ++d) {
let week = day.week();
// небольшой хак, момент считает
// последние дни декабря за первую неделю,
// но мне надо чтобы считалось за 53
if (m === 11 && week === 1) {
week = 53
}
// если неделя еще не присутствует в месяце, то добавляем ее
if (!month.weeks.hasOwnProperty(week)) {
month.weeks[week] = {}
}
// добавляем день, у weekday() нумерация с нуля,
// поэтому добавляю единицу, можно и не добавлять,
// но так будет удобнее
month.weeks[week][day.weekday() + 1] = {
date: day.toDate(),
};
// итерируем день на единицу, moment мутирует исходное значение
day.add(1, 'd');
}
// добавлям данные по месяцу в год
data.push(month);
}
return data
}
}
...
}
Можно заглянуть в vue-devtools и увидеть там:
Верстаем
Ну давайте чего-нибудь уже выведем. Сначала научимся верстать один месяц, а потом, как освоимся, выведем все остальные. В общем, правим шаблон Calendar.vue:
{{yearData[0].title}}
{{week[day].date.getDate()}}
Сначала заставим отображать даты в нашей неделе в ряд, для этого поправим стиль:
теперь укажем, что каждый день должен занимать одинаковое количество места в нашем ряду:
Ну вроде поприличнее стало, только цифры таки скачут. Происходит это потому, что flex-grow по сути распределяет пустое пространство, а текст цифр в это пустое пространство не входит, поэтому, чтобы ячейки с цифрами стали действительно равными надо указать в стиле, чтобы ширина текста не учитывалась. Для этого установим свойству flex-basis на ноль.
Если не совсем понятно что я говорю, попробуйте поизучать данную картинку:
Ну как? Правим стиль:
от теперь красота
Я думаю мы теперь готовы к тому, чтобы попробовать вывести все месяцы, правим шаблон:
{{month.title}}
{{week[day].date.getDate()}}
Отлично, у нас уже своего рода респонсивный календарь:
Но нам этого мало, у нас календарь отображается в столбик, как завещал дедушка div, а нам бы в строчку… Сделаем по аналоги. Только что мы каждую неделю назначили flex контейнером для ее дней. А теперь наш блок year назначим flex контейнером для его месяцев. Добавим стили:
чет, каша какая-та:
причина сей каши в том, что по умолчанию flex не делает переносов, а пытается все отобразить в одну строчку, ну и соответственно сжимает покуда сил хватает, а их не хватает. Чтобы включить режим переносов, надо в нашем контейнер year добавить свойство flex-wrap, сделаем это:
Ну, эээ… типа получше стало, хотя б переносит:
Тут воздух маловато, но нам кстати так даже интереснее. Очевидно, что лучше всего чувствуют себя ноябрь и декабрь, но так как мы за равенство, то давайте ужмем их, чтобы сильно не выделялись.
Чтобы ужать, надо убрать flex-grow: 1 у month, (ток добавили, теперь удалять…), который отвечает за растяжение в рамках строки:
За то как будут располагаться последние два (на самом деле не только за них) висящих элемента отвечает justify-content в стиле контейнере, по умолчанию он равен flex-start. Можно выровнять в конец.
Вот гифка с разными значениями:
Так как я планирую, что у меня будет всегда одинаковое количество месяцев в строке, и хочу чтобы они занимали все свободное место, то я пожалуй верну flex-grow: 1; обратно, и добавлю немного воздуха:
красота:
Еще раз вернусь к justify-content и flex-grow: 1. Сравните две гифки, на первой у month flex-grow = 1, на второй — свойство отсутствует:
Какой вариант вам больше по душе, решайте сами.
Добавим строчку с днями недели. Сначала добавим вычислимое свойство в скрипт
export default {
...
computed: {
weekDays () { // дни недели
let days = [];
for(let i = 1; i<=7;++i) {
days.push(moment().isoWeekday(i).format("dd"))
}
return days;
},
...
}
}
а теперь отобразим их в шаблоне:
{{month.title}}
{{d}}
{{week[day].date.getDate()}}
Я хочу чтобы воскресенье у меня было красненькое, давайте добавим динамический стиль к узлу .day:
...
{{week[day].date.getDate()}}
А теперь подкорректируем стили, чуток красоты наведем:
Ну и последние штрихи: добавим возможность менять год и сделаем фиксированный заголовок средствами flex.
Переключимся на App.vue файл, и откорректируем шаблон:
{{year}}
добавилась строчка с годом, пока, как видно, не фиксированная:
Подправим стили в App.vue, уберем отступы в body, установим высоту html и body на всю высоту окна, и сделаем заголовок покрасивше, я намеренно использую два узла style, один для глобальных стилей второй для локальных:
Идея создания фиксированного заголовка на flex заключается в использовании двух вложенных контейнеров flex, один из которых ограничивает высоту всего содержимого, а второй, вложенный, использует flex-direction: column.
Правим стиль:
Классно, да? Вы можете даже сделать футер:
{{year}}
{{year}}
Ну и давайте кнопки для переключения года добавим:
{{year}}
{{year}}
Воспользуемся уже полученными знаниями, и сделаем заголовок более flex-образным, правим стили:
Хм… что-то тут не так. Чет наши заголовки прям сдавило и верстка поплыла. К сожалению это тот момент, который я не до конца понял почему так произошло. Но как я полагаю, это из-за того что display: flex задает динамическую высоту, и находясь внутри другого flex контейнера, ориентируется на размеры заданные своим родителем.
В общем, чтобы это вылечить, надо запретить flex контейнеру внутри которого находится наш header сжимать его размеры, для этого добавим свойство flex-shrink:
Ну вот и все, теперь у вас есть flex-календарь на любой год!
В это статье не удалось показать все возможности flex, но общий подход работы с ним, думаю, отразить получилось.
Я надеюсь, что статья поможет тем, кто как и я застрял в css-временах где-то между 3-м и 4-м бутстрапом, сделать свои первые шаги навстречу современному css.
Код примера доступен по адресу.