Gone are the days when you could just throw a few CSS definitions in something called ‘style.css’ and call it a good day’s work. With the arrival of semantic html where none of the styling is done inline it has become important to practice good coding habits while creating CSS styles.
It was embarrasingly recently that I had the habit of putting all my CSS into one file. I didn’t realize that it’s the same bad habit of making programs with only one (usually massive) file of source code. Learning to partition the code properly is a natural development step and it’s just as important for CSS as Ruby, Java, C, or any other.
How I do CSS:
—-
What I’ve found is I have a few default CSS files named after the following pattern:
- application.css: usually will overwrite bad default values (like margins on forms)
- structure.css: the layout of the blocks that make up my layout (body, #wrap, #head, #main, #foot, etc.)
- fonts.css: font-family, font-size, etc. for all the popular styles
- forms.css: whether using label/input pairs or definition lists I need lots of styling for forms
- colors.css: super important to keep all color stuff together so you can both keep the colors in harmony and replace this file with some alternate color theme
These worked for a while but then I realized that I had little pieces of code that were needed that didn’t belong in any of these. I need to specify that a certain logo floats to the right or a users profile just happens to need a different background color.
The answer to this comes in using CSS files kinda like Rails uses partials. Some encapsulized code that has usefulness in a certain portion of your app. I did this at first by making stylesheets named after each of my controllers. This worked for about three seconds until I realized that I needed some conditional way to include them.
Conditionally including CSS files
—-
Including stylesheets conditionally turned out to be a fairly easy task. I’ll outline the solution here using extractions from my code:
1
2
3
4
5
6
|
class ApplicationController < ActionController::Base
attr_accessor :stylesheets
before_filter {|c| c.stylesheets ||= []; c.stylesheets << c.class.name.sub('Controller','').downcase }
end |
This adds an instance var @stylesheets that starts as an array. It is given, by default the name of the currently active controller. note: this prints the derived class’s name, not ‘application’.
This means that you can now add stylesheets to the soon-to-be-rendered view anywhere in your code you want.
1
2
3
4
5
6
|
class UsersController < ApplicationController
# if you have a stylesheet that governs just one action
def show
stylesheets << 'users_show'
end
end |
And it’s surprisingly simple to output the required code to the browser. Just add one line to your app/views/layouts/application.rhtml file:
1
2
3
4
5
6
7
|
<%= stylesheet_link_tag 'application', :media => 'all' %>
<%= stylesheet_link_tag 'structure'', :media => 'all' %>
<%= stylesheet_link_tag 'fonts'', :media => 'all' %>
<%= stylesheet_link_tag 'forms', :media => 'all' %>
<%= stylesheet_link_tag 'colors', :media => 'all' %>
<%= @stylesheets.collect { |file| stylesheet_link_tag file }.join("\n") if @stylesheets %>
<%= javascript_include_tag :defaults %> |
This is so simple that there’s no real point in turning it into a plugin but if anybody wants it that way just let me know.
Update: The version I ended up using in my app did some testing for the existence of the file before it included it as a stylesheet. I decided it was more important to avoid the 404 errors (especially the way they cruft up the logfile) than it was to have the app disk-optimized. So here’s my new application_controller:
1
2
3
4
5
6
7
8
9
10
|
class ApplicationController < ActionController::Base
attr_accessor :stylesheets
before_filter do |c|
c.stylesheets ||= []
klass = c.class.name.sub('Controller','').downcase
c.stylesheets << klass if File.exists?("#{RAILS_ROOT}/public/stylesheets/#{klass}.css")
end
end |
I decided to try filling up my ./test/integration folder today because it was empty and I felt I must have been missing out. I can’t stand the idea that all the other Rails devs are enjoying some feature I haven’t found out about yet.
Integration tests seem pretty handy because functional tests are pretty basic – effectively unit tests for controllers rather than tests of real interactivity. So I figured I’d google around a bit to discover this integration testing thing and this is what I found:
(which currently doesn’t have a web server so I had to grab the page from Google’s cache).
That’s it. Those three meager pages are all the info I could find on Rails integration testing. In fact, I have reason to believe that the Rails Recipes book just re-used the code from Jamis Buck’s post – so even they don’t have any more for us.
Well, that’s not acceptable. I’m trying to follow a particular user’s experience through my app and I want to get this working without having to resort to SeleniumOnRails or some other browser-emulator.
My code so far
I’ve got a particular user type `Athlete` that logs in and checks their profile. They update a couple pieces of information and we check that the info was saved. Should be easy. (note: to understand this code check out Jamis’s article – you might need to look in the Google Cache for it)
class AthleteSessionTest < ActionController::IntegrationTest
fixtures :users,
:athlete_contacts,
:coach_contacts,
:junior_colleges,
:combines
def test_randy_visits
randy = new_session_as('Cliffton Player')
randy.goes_to_profile_edit
randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
randy.
end
private
module AthleteSessionDSL
def goes_to(place)
get place
end
def goes_to_login
get '/auth/login'
assert_response :success
assert_template 'auth/login'
end
def goes_to_profile_edit
get '/athletes/profile_edit'
assert_response :success
assert_template 'athletes/profile_edit'
end
def logs_in_as(athlete)
@current_user = users(athlete)
post 'auth/login', :user => {:email => @current_user.email, :password => @current_user.password }
assert_response :redirect
follow_redirect!
assert_template 'athletes/profile'
end
def updates_profile_with(contact)
post 'athletes/update_contact', :contact => contact,
:athlete_id => @current_user.id
assert_flash_equal 'This profile has been updated with new information', :success
assert_response :redirect
follow_redirect!
assert_template 'athletes/profile'
contact.each { |k,v| assert_equal v, @current_user.profile.attributes[k.to_s] }
end
end
end
def new_session
open_session do |sess|
sess.extend(AthleteSessionDSL)
yield sess if block_given?
end
end
def new_session_as(person)
new_session do |sess|
sess.goes_to_login
sess.logs_in_as(person)
yield sess if block_given?
end
end
end
The Problem
So this is fine. So far it’s working great. The problem is that I’ve set it up so that everything my user does is part of my AthleteSessionDSL module in it’s own method. Everything `randy` does has to be a method within the `AthleteSessionDSL` module.
I wanted to try some non-modular navigation to give Randy a bit more of a free spirit. I thought I’d see if I could take some of the functionality out of the module and do it directly:
def test_randy_visits
randy = new_session_as('Cliffton Player')
randy.goes_to_profile_edit
randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
# trying some direct session navigation outside of the randy session object
get 'athletes/profile'
assert_response :success
assert_template 'athletes/profile'
end
What happened? Well, since `randy` is really just an instance of a Rails session with some handy methods attached to it I immediately left the session when I stopped using `randy`. My request to get the “athletes/profile” page resulted in a redirect to my login page because that method is authentication protected.
Well, what’s the solution? Do I have to make a million methods for everything I want Randy to do?
The Solution
It turns out to be quite simple. The methods we have been using like `assert_response` and `get` (and all the rest) are, in fact, methods of the session. The `open_session` call in the `new_session` module creates a session object with all the standard methods on it.
So not only can we group functionality into methods and roll those into included modules – we can also call things directly:
def test_randy_visits
randy = new_session_as('Cliffton Player')
randy.goes_to_profile_edit
randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
randy.get '/athletes/profile'
randy.assert_response :success
randy.assert_template 'athletes/profile'
end