I recently upgraded TroopTrack from Twitter Bootstrap 1 to 2, redesigning a few key aspects of the site to make it more usable. Overall, I was very pleased with the results except for one area – the calendar. I was using a calendar gem that provided a view helper that generates a table based calendar. It’s worked okay in the past, but now that I had managed to get every other aspect of my site scaling nicely for display on tablets, the horizontal scrolling caused by the rigid table layout was more than I could bear.
What I really needed was a table-less calendar that uses the bootstrap grid. The problem is, a 12-grid layout isn’t very good for creating a calendar, unless I manage to convince the universe to have either six or twelve days in a week. A quick call to my senator to that option off the table, so I decided to create a 7 column grid.
This is totally easy with Bootstrap’s form for downloading a customized CSS. I tweaked things a bit to get rid of the gutters, turned off pretty much everything except the grid bits, then wrapped the resulting CSS in a selector so it would only apply to my calendar div.
That was the easy part.
Next I needed a helper I could use to create my calendar. Fortunately, Ryan Bates had revised his screencast on how to create a calendar back in August and I was lucky enough to remember watching it. I swiped his calendar helper code and was off to the races.
I’m pretty happy with the end result – a 100% table free calendar layout that is uber-responsive and looks great on my iPad Mini (which I love!).
Steal My Code
It’s not like it’s mine anyway… I’d like to turn this into a gem, but for now here you go.
Helper
module BootstrapCalendarHelper
def bootstrap_calendar(date = Date.today, &block)
Calendar.new(self, date, block).calendar_div
end
class Calendar < Struct.new(:view, :date, :callback)
HEADER = %w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]
START_DAY = :sunday
delegate :content_tag, to: :view
def calendar_div
content_tag 'div', class: "calendar_grid" do
header + week_rows
end
end
def header
content_tag 'div', class: 'month_header row-fluid' do
HEADER.map { |day| content_tag :div, class: 'span1' do
day
end }.join.html_safe
end
end
def week_rows
weeks.map {|week|
content_tag :div, class: 'row-fluid week' do
week.map { |day| day_cell(day) }.join.html_safe
end
}.join.html_safe
end
def day_cell(day)
content_tag :div, view.capture(day, &callback), class: day_classes(day)
end
def day_classes(day)
classes = ['span1']
classes << "today" if day == Date.today
classes << "notmonth" if day.month != date.month
classes << "month" if day.month == date.month
classes.empty? ? nil : classes.join(" ")
end
def weeks
first = date.beginning_of_month.beginning_of_week(START_DAY)
last = date.end_of_month.end_of_week(START_DAY)
(first..last).to_a.in_groups_of(7)
end
end
def event_style(event)
"background-color: #{event.color};"
end
def event_link_style(event)
if %w(white silver yellow lime aqua teal fuchsia).include?(event.color)
"color: black;"
else
"color: white;"
end
end
end
CSS
This is mostly the CSS generated by Bootstrap, with a few things I threw in to make it look like a calendar.
.calendar_grid {
border: 1px solid gray;
margin-bottom: 30px;
.month_header {
background-color: gray;
.span1 {
padding-top: 5px;
font-weight: bold;
text-align: center;
}
}
.notmonth {
background-color: darken(#ededed, 5%);
}
.notmonth, .notmonth a { color: white; }
.month.today {
background-color: #D7F2FF;
}
.month {
a {
color: gray;
}
background-color: #ededed;
border: 1px solid whitesmoke;
}
.week .span1 {
padding: 2px;
// height: 100px;
ul.event_summary {
margin: 0;
li {
list-style: none;
padding: 2px;
-moz-border-radius:4px;
-webkit-border-radius:4px;
border-radius:4px;
}
}
}
/*!
* Bootstrap v2.2.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
line-height: 0;
}
.clearfix:after {
clear: both;
}
.hide-text {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.input-block-level {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.row {
margin-left: -1px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
content: "";
line-height: 0;
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 1px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 958px;
}
.span7 {
width: 958px;
}
.span6 {
width: 821px;
}
.span5 {
width: 684px;
}
.span4 {
width: 547px;
}
.span3 {
width: 410px;
}
.span2 {
width: 273px;
}
.span1 {
width: 136px;
}
.offset7 {
margin-left: 960px;
}
.offset6 {
margin-left: 823px;
}
.offset5 {
margin-left: 686px;
}
.offset4 {
margin-left: 549px;
}
.offset3 {
margin-left: 412px;
}
.offset2 {
margin-left: 275px;
}
.offset1 {
margin-left: 138px;
}
.row-fluid {
width: 100%;
*zoom: 1;
}
.row-fluid:before,
.row-fluid:after {
display: table;
content: "";
line-height: 0;
}
.row-fluid:after {
clear: both;
}
.row-fluid [class*="span"] {
display: block;
width: 100%;
min-height: 30px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
float: left;
margin-left: 0.10438413361169101%;
*margin-left: 0.052192066805845504%;
}
.row-fluid [class*="span"]:first-child {
margin-left: 0;
}
.row-fluid .controls-row [class*="span"] + [class*="span"] {
margin-left: 0.10438413361169101%;
}
.row-fluid .span7 {
width: 100%;
*width: 99.94780793319416%;
}
.row-fluid .span6 {
width: 85.69937369519833%;
*width: 85.64718162839249%;
}
.row-fluid .span5 {
width: 71.39874739039666%;
*width: 71.34655532359082%;
}
.row-fluid .span4 {
width: 57.09812108559499%;
*width: 57.045929018789145%;
}
.row-fluid .span3 {
width: 42.79749478079332%;
*width: 42.74530271398748%;
}
.row-fluid .span2 {
width: 28.49686847599165%;
*width: 28.444676409185806%;
}
.row-fluid .span1 {
width: 14.19624217118998%;
*width: 14.144050104384133%;
}
.row-fluid .offset7 {
margin-left: 100.20876826722338%;
*margin-left: 100.10438413361169%;
}
.row-fluid .offset7:first-child {
margin-left: 100.10438413361169%;
*margin-left: 100%;
}
.row-fluid .offset6 {
margin-left: 85.90814196242171%;
*margin-left: 85.80375782881002%;
}
.row-fluid .offset6:first-child {
margin-left: 85.80375782881002%;
*margin-left: 85.69937369519833%;
}
.row-fluid .offset5 {
margin-left: 71.60751565762004%;
*margin-left: 71.50313152400835%;
}
.row-fluid .offset5:first-child {
margin-left: 71.50313152400835%;
*margin-left: 71.39874739039666%;
}
.row-fluid .offset4 {
margin-left: 57.306889352818374%;
*margin-left: 57.202505219206685%;
}
.row-fluid .offset4:first-child {
margin-left: 57.20250521920668%;
*margin-left: 57.09812108559499%;
}
.row-fluid .offset3 {
margin-left: 43.006263048016706%;
*margin-left: 42.90187891440502%;
}
.row-fluid .offset3:first-child {
margin-left: 42.90187891440501%;
*margin-left: 42.79749478079332%;
}
.row-fluid .offset2 {
margin-left: 28.70563674321503%;
*margin-left: 28.601252609603343%;
}
.row-fluid .offset2:first-child {
margin-left: 28.601252609603343%;
*margin-left: 28.496868475991654%;
}
.row-fluid .offset1 {
margin-left: 14.405010438413361%;
*margin-left: 14.30062630480167%;
}
.row-fluid .offset1:first-child {
margin-left: 14.30062630480167%;
*margin-left: 14.196242171189978%;
}
[class*="span"].hide,
.row-fluid [class*="span"].hide {
display: none;
}
[class*="span"].pull-right,
.row-fluid [class*="span"].pull-right {
float: right;
}
}
View Code Example
= bootstrap_calendar month do |date|
= link_to date.day, new_plan_event_path(:event => {:activity_at => date.beginning_of_day + 12.hours})
- if @events_by_date[date]
ul.event_summary
- @events_by_date[date].each do |event|
li style=event_style(event)
= link_to "#{event.title}: #{event.activity_at.to_s(:time_only)}", menu_plan_event_path(event), :remote => true, :style => event_link_style(event)
Update – the morning after
I got a little crazy last night and gemified this puppy: http://rubygems.org/gems/twitter-bootstrap-calendar





6 responses so far ↓
1 Aaron Trevena // Dec 22, 2012 at 10:28 am
Hi David,
Can you post some example HTML output? The ruby code is no use to me, but this is the only nice-ish looking calendar I’ve seen for bootstrap.
Thanks,
A
2 Aaron Trevena // Dec 22, 2012 at 10:52 am
Bah – my first comment probably comes accross as rather abrupt and ungrateful.
What I mean is that I’ve been looking for a nice-ish calendar for bootstrap and the screenshot you link to is the first I’ve seen that meets that criteria – obviously it’s better than nice-ish
Might I suggest or request you put a sample of the html generated up on bootsnipp or somewhere so that people not using ruby on rails (i.e. 99.9% of developers) can make use of what looks a pretty good calendar layout.
Thanks,
Aaron (@hashbangperl on twitter)
3 David Christiansen // Dec 22, 2012 at 11:20 am
I got it, first try. I’m not sensitive.
I’m not sure if this will be much help unless you go in and pretty it up a bit, but here’s the source generated by that helper:
https://gist.github.com/4359680
Best of luck!
Dave
4 Aaron Trevena // Dec 23, 2012 at 2:10 am
Nice xmas present, cheers!
5 Aaron Trevena // Dec 27, 2012 at 12:16 pm
A little bit of restyling later and I’m fairly happy with the result :https://twitter.com/hashbangperl/status/284345399576252417/photo/1 – now integrating into my Perl App using Catalyst, Template Toolkit, DBIx::Class and Calendar::List – should be a breeze
6 Longchi // Mar 24, 2013 at 3:51 pm
Hi Aaron it it possible to get hold of your twitter bootstrap Calendar frontend scripts(html/css/js) I have also been looking for one but have found anything decent. thanks
Leave a Comment