Information Technology Dark Side

Struggles of a Self-Taught Coder

Information Technology Dark Side header image 2

Creating a Calendar with Twitter Bootstrap WITHOUT TABLES

December 10th, 2012 · 6 Comments

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

If you enjoyed this post, make sure you subscribe to my RSS feed!
Stumble it!

Tags: Uncategorized

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