Хабрахабр

[Из песочницы] Верстаем flex-календарик

Идет 2018 год, модные пацаны давно уже верстают на grid, а я все на третьем бутстрапе сижу с col-md кочерячусь, мельком поглядывая на четвертый.

Решил я, что это не дело, и стоит немного знания освежить, но у grid вроде как поддержка пока хромает, а вот flex технологию уже даже утюги поддерживают.

И процессом усвоения с вами поделится. Вот и решил его освоить. В общем, будем верстать календарик на весь год.

Нам потребуется

  • vue
  • клей moment
  • и чуток flex

Результат будет выглядеть примерно вот так:

Для работы с датами буду использовать библиотечку moment, можно и без нее обойтись, но мне с ней привычнее. Так получилось, что я адепт vue, и поэтому для рендера буду использовать его.

Подготовка

Устанавливаем 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

<template> </template> <script>
export default }
}
</script> <style> </style>

Чтоб не показаться варваром, не будем просто верстать голый календарь, а разработаем отдельную компоненту.

Создадим файлик Calendar.vue:

<template> <div>Календарь</div>
</template> <style lang="scss" scoped> </style> <script> export default { props: { year: { // год на который строится календарь type: Number, default: (new Date()).getFullYear() }, }, data () { return {} } }
</script>

подключим компоненту глобально в 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

<template> <calendar></calendar>
</template>
...

Если все верно сделали, то увидим слово "Календарь" на белом фоне.

Готовим данные

Я предлагаю упихать данные по году в массив из месяцев. Прежде чем что-то рисовать надо подготовить данные для календаря. В свою очередь, каждый месяц будет представлять собой объект вида:

{ 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:

<template> <div class="month"> <div class="title">{{yearData[0].title}}</div> <div class="week" v-for="week in yearData[0].weeks"> <div class="day" v-for="day in 7"> <span v-if="week[day]">{{week[day].date.getDate()}}</span> </div> </div> </div>
</template>

Сначала заставим отображать даты в нашей неделе в ряд, для этого поправим стиль:

<style lang="scss" scoped> .week { display: flex; }
</style>

теперь укажем, что каждый день должен занимать одинаковое количество места в нашем ряду:

<style lang="scss" scoped> .week { display: flex; } .day { flex-grow: 1; }
</style>

Происходит это потому, что flex-grow по сути распределяет пустое пространство, а текст цифр в это пустое пространство не входит, поэтому, чтобы ячейки с цифрами стали действительно равными надо указать в стиле, чтобы ширина текста не учитывалась. Ну вроде поприличнее стало, только цифры таки скачут. Для этого установим свойству flex-basis на ноль.

Если не совсем понятно что я говорю, попробуйте поизучать данную картинку:

image

Правим стиль: Ну как?

<style lang="scss" scoped> .week { display: flex; } .day { flex-grow: 1; flex-basis: 0; }
</style>

от теперь красота

Я думаю мы теперь готовы к тому, чтобы попробовать вывести все месяцы, правим шаблон:

<template> <div class="year"> <div class="month" v-for="month in yearData"> <div class="title">{{month.title}}</div> <div class="week" v-for="week in month.weeks"> <div class="day" v-for="day in 7"> <span v-if="week[day]">{{week[day].date.getDate()}}</span> </div> </div> </div> </div>
</template>

Отлично, у нас уже своего рода респонсивный календарь:

Только что мы каждую неделю назначили flex контейнером для ее дней. Но нам этого мало, у нас календарь отображается в столбик, как завещал дедушка div, а нам бы в строчку… Сделаем по аналоги. Добавим стили: А теперь наш блок year назначим flex контейнером для его месяцев.

<style lang="scss" scoped> .week {...} .day {...} .year { display: flex; } .month { flex-grow: 1; flex-basis: 0; }
</style>

чет, каша какая-та:

Чтобы включить режим переносов, надо в нашем контейнер year добавить свойство flex-wrap, сделаем это: причина сей каши в том, что по умолчанию flex не делает переносов, а пытается все отобразить в одну строчку, ну и соответственно сжимает покуда сил хватает, а их не хватает.

<style lang="scss" scoped> .week {...} .day {...} .year { display: flex; flex-wrap: wrap; // добавили } .month {...}
</style>

Ну, эээ… типа получше стало, хотя б переносит:

Очевидно, что лучше всего чувствуют себя ноябрь и декабрь, но так как мы за равенство, то давайте ужмем их, чтобы сильно не выделялись. Тут воздух маловато, но нам кстати так даже интереснее.

Чтобы ужать, надо убрать flex-grow: 1 у month, (ток добавили, теперь удалять...), который отвечает за растяжение в рамках строки:

<style lang="scss" scoped> ... .month { flex-basis: 0; }
</style>

Можно выровнять в конец. За то как будут располагаться последние два (на самом деле не только за них) висящих элемента отвечает justify-content в стиле контейнере, по умолчанию он равен flex-start.

<style lang="scss" scoped> .week {...} .day {...} .year { display: flex; flex-wrap: wrap; justify-content: flex-end; // выровнять в конец } .month {...}
</style>

Вот гифка с разными значениями:

Так как я планирую, что у меня будет всегда одинаковое количество месяцев в строке, и хочу чтобы они занимали все свободное место, то я пожалуй верну flex-grow: 1; обратно, и добавлю немного воздуха:

<style lang="scss" scoped> .week {...} .day { margin: 0.25em; // воздух flex-grow: 1; flex-basis: 0; } .year { display: flex; flex-wrap: wrap; } .month { margin: 0.25em; // воздух flex-basis: 0; flex-grow: 1; // вернул обратно }
</style>

красота:

Сравните две гифки, на первой у month flex-grow = 1, на второй — свойство отсутствует: Еще раз вернусь к justify-content и 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; }, ... } }

а теперь отобразим их в шаблоне:

<template> <div class="year"> <div class="month" v-for="month in yearData"> <div class="title">{{month.title}}</div> <div class="week"> <div class="day" v-for="d in weekDays"> <span>{{d}}</span> </div> </div> <div class="week" v-for="week in month.weeks"> <div class="day" v-for="day in 7"> <span v-if="week[day]">{{week[day].date.getDate()}}</span> </div> </div> </div> </div>
</template>

Я хочу чтобы воскресенье у меня было красненькое, давайте добавим динамический стиль к узлу .day:

<template> <div class="year"> <div class="month" v-for="month in yearData"> ... <div class="week" v-for="week in month.weeks"> <div class="day" v-for="day in 7" :class="{[`week-day-${day}`]: true}"> <span v-if="week[day]">{{week[day].date.getDate()}}</span> </div> </div> </div> </div>
</template>

А теперь подкорректируем стили, чуток красоты наведем:

<style lang="scss" scoped> .title { // новый стиль под название месяца margin: 0.25em; font-weight: bold; } .week-day-7 { // воскресенье color: red; } ...
</style>

Ну и последние штрихи: добавим возможность менять год и сделаем фиксированный заголовок средствами flex.

Переключимся на App.vue файл, и откорректируем шаблон:

<template> <div class="wrapper"> <div class="content"> <div class="header"> <div class="title">{{year}}</div> </div> <div class="body"> <calendar :year="year"></calendar> </div> </div> </div>
</template>

добавилась строчка с годом, пока, как видно, не фиксированная:

Подправим стили в App.vue, уберем отступы в body, установим высоту html и body на всю высоту окна, и сделаем заголовок покрасивше, я намеренно использую два узла style, один для глобальных стилей второй для локальных:

<style lang="scss"> html { height: 100%; } body { height: 100%; margin: 0; }
</style> <style lang="scss" scoped> .title { font-weight: bold; font-size: 1.5em; margin: 0.25em; text-align: center; }
</style>

Идея создания фиксированного заголовка на flex заключается в использовании двух вложенных контейнеров flex, один из которых ограничивает высоту всего содержимого, а второй, вложенный, использует flex-direction: column.

Правим стиль:

<style lang="scss" scoped> .title {...} .wrapper { // ограничивает высоту display: flex; height: 100%; // тут я указываю высоту по высоте родительского узла, в нашем случае 'это тег body } .content { // непосредственный контейнер с заголовком и содержимым display: flex; flex-direction: column; } .body { // основное тело контейнера flex-grow: 1; // растягивается, чтобы заполнить все пространство overflow-y: auto; // скролл, если не влезает }
</style>

Вы можете даже сделать футер: Классно, да?

<template> <div class="wrapper"> <div class="content"> <div class="header"> <div class="title">{{year}}</div> </div> <div class="body"> <calendar :year="year"></calendar> </div> <div class="header"> <div class="title">{{year}}</div> </div> </div> </div>
</template>

Ну и давайте кнопки для переключения года добавим:

<template> <div class="wrapper"> <div class="content"> <div class="header"> <button @click="--year">&lt;</button> <div class="title">{{year}}</div> <button @click="++year">&gt;</button> </div> <div class="body"> <calendar :year="year"></calendar> </div> <div class="header"> <div class="title">{{year}}</div> </div> </div> </div>
</template>

Воспользуемся уже полученными знаниями, и сделаем заголовок более flex-образным, правим стили:

<style lang="scss" scoped> .title {...} .header { padding: 0.25em; display: flex; justify-content: space-between; } .wrapper {...} .content {...} .body {...}
</style>

Чет наши заголовки прям сдавило и верстка поплыла. Хм… что-то тут не так. Но как я полагаю, это из-за того что display: flex задает динамическую высоту, и находясь внутри другого flex контейнера, ориентируется на размеры заданные своим родителем. К сожалению это тот момент, который я не до конца понял почему так произошло.

В общем, чтобы это вылечить, надо запретить flex контейнеру внутри которого находится наш header сжимать его размеры, для этого добавим свойство flex-shrink:

<style lang="scss" scoped> .title {...} .header { padding: 0.25em; display: flex; flex-shrink: 0; // не сжимай меня justify-content: space-between; } .wrapper {...} .content {...} .body {...}
</style>

Ну вот и все, теперь у вас есть flex-календарь на любой год!

В это статье не удалось показать все возможности flex, но общий подход работы с ним, думаю, отразить получилось.

Я надеюсь, что статья поможет тем, кто как и я застрял в css-временах где-то между 3-м и 4-м бутстрапом, сделать свои первые шаги навстречу современному css.

Код примера доступен по адресу.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть