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