Skip to content

Instantly share code, notes, and snippets.

@astashov
Created September 3, 2011 08:44
Show Gist options
  • Save astashov/1190877 to your computer and use it in GitHub Desktop.
Save astashov/1190877 to your computer and use it in GitHub Desktop.
General Organizational Principles for CSS and JS in Rails
==== General Organizational Principles for CSS and JS in Rails
=== Framework requirements
1. Modularity
2. Complex components are built from simple, atomic components
3. Cross-browser compatibility
a. Follow W3C standards
b. Keep IE hacks in a separate style file
4. Bulletproof
a. Components are independent of their surroundings
b. They work even with disabled Javascript (if they use Javascript)
c. They are accessible even with disabled images
5. Expansibility
a. The same rules apply at all levels of the hierarchy
6. Absolutely unobtrusive
=== Approach
Key idea: *independent blocks*
We split the page into non-overlapping, non-interfering modules. They can be nested. These are the independent blocks.
Every block has a name.
Conditions of independence
1. Minimum of global styles
2. Don't use 'id', only 'class'. You can use 'id' only if you:
a. ABSOLUTELY SURE there will be only one such block on the page
b. Going to add JS to the block - looking for a block by 'id' is significantly faster
3. Name of block starts with prefix (.b-mega-block instead of .mega-block)
4. There are no classes outside of blocks
All JS behavior occurring in the blocks should be encapsulated within the blocks' code. Same goes
for styles. Please avoid global styles and global JS behavior. If some behavior repeats frequently,
move it to a separate mixin and just include it in the blocks that need it.
Prefixes: b - for block, l - for layout fragments, m - mixins.
=== Types of classes
1. Blocks - b
Blocks are simple or complex (complex = composed of simple) independent modules.
Generally, 80% of all markup of the page are blocks.
Astro-alerts, Tours, Polls, etc are blocks.
2. Layouts - l
Layouts - Divs/tables that are used only for creating structure of the page.
E.g., when we need a 3-column layout, we will have .l-left, .l-center, .l-right.
Generally, blocks are placed in these layouts.
3. Mixins - m
Mixins are like modules (mixins) you include in classes in Ruby.
E.g., .m-clear is the clearfix for the floats, .m-label-on-input moves labels over input,
.m-calendar adds calendar to some block, .m-carousel makes carousel from ul/li list.
=== Structure of directories.
The following directory structure is used within each Rails application.
== Blocks
Every block is stored in a separate directory. Different parts of blocks (CSS, JS, HTML) are stored in different parts of the app. E.g.,
block b-something can be placed in any of these directories:
/stylesheets/framework/block/something/ - stylesheets
/javascripts/framework/block/something/ - javascript logic for the block
/images/framework/block/something/ - images for the block
/app/views/framework/block/something/ - for the HTML markup of the block
Of course, if the block doesn't have javascript logic, it will not have /javascripts/block/something directory.
1. Stylesheets:
The stylesheets directory can contain:
b-something.css - main file, for all browsers.
b-something.ie.css - styles for all IE browsers.
b-something.reset.css - reset styles for all browsers.
b-something.css is always included.
IE styles are included using conditional comments.
Reset styles file is included if the site doesn't have a global "reset styles" file.
So, a stylesheet directory might look like this:
stylesheets/
framework/
block/
something/
b-something.css - main CSS file of the block
b-something.ie.css - styles for IE
b-something.reset.css - reset file for the block
Sometimes you want to make use of the same block, but with a slightly different look on different pages.
We do this by adding an additional class.
E.g., we have b-something. We need modifications: super. We add
additional classes .b-something_super. Then, we put main styles in .b-something,
and add additional styles to .b-something_super.
For additional classes (modifications):
block/
something/
super/
b-something_super.css
b-something_super.ie.css
b-something.css
b-something.ie.css
...newFolder
There should be a script, that scans public dir and generates files application.js and application.css from css and js files within public/framework dir.
Where performance won't be impacted (i.e. if you don't need the HTML element to be quickly accessible by DOM search), you should always use classes to refer to DOM-elements, and these classes should
use a prefix which is the name of the block. E.g., classes might look like the following:
.b-something { }
.b-something_title { }
.b-something_contents { }
You may use tag names and classes without a prefix only if you are sure that this block will not contain other blocks or that block will not be nested in other blocks.
I.e.:
.b-something
.data
.data_title
.data_title_subtitle
But if your block is going to have other nested blocks, or is going to be nested itself, better use "with-prefix" class names:
.b-something
.b-something_data
.b-something_data_title
.b-something_data_title_subtitle
Usually, blocks like 'b-birthday-form', 'b-login-form', etc. should have "with-prefix" class names (i.e. which will be used in many places on the page and most probably within other blocks), but blocks like 'b-scopesboard', or 'b-syndication_rss' will be used only on some specific pages and they may contain "without-prefix" class names.
Notes:
a. Use of '-' versus '_'
You should use '-' as space and '_' as separator (from name of block, or from subclass). Examples:
.b-something_header - class of header of b-something,
.b-something-cool_header - class of header of b-something-cool
.b-something_cool-header - class of cool header of b-something
.b-something_super_header - class of header of modification super of block b-something (or the subblock 'header' of the parent block 'super' of the main block 'b-something')
b. Abstract blocks
Sometimes we have very similar blocks with some common styles.
In such cases, we can add an abstract block, that is not used without modifiers.
Common styles are described in the abstract block; other styles in the modified
blocks (analogous idea to OOP - inheritance from abstract class).
In general, this is a good idea - apply OOP principles to the creation of xHTML/CSS.
2. Javascript
If block has JS logic, it should have a javascript file there: /javascripts/framework/block/something/b-something.js
JS logic implementation should follow the jQuery approach. Example of a template:
(function($) {
var classes = {
some_class: 'b-something_some-class'
// Other classes
};
$.fn.b_something = function(options) {
var opts = $.extend({}, $.fn.b_something.defaults, options);
return this.each(function() {
var self = $(this);
// Code of the JS logic
});
};
$.fn.b_something.defaults = {
// Defaults
};
})(jQuery);
After that, you have to call this function in the HAML block's template:
#b-something.b-something
.b-something_some-class
...
= javascript_tag("jQuery('#b-something').b_something();")
This way of calling JS code is preferable (instead of $(function () {..});), since with the $(function () {...}); way it will wait until a page is loaded,
and therefore it looks like the page is loading slower. It is better when JS behaviour is accessible immediatly after the
block's DOM is loaded.
It is recommended that you use *variables* with class names (like in the example above) instead of hard-coded classes in
jQuery selectors and overall JavaScript code.
JS should try to work with DOM using only classes of the current block.
I.e., you should try to avoid including $('.b-another').hide(); or similar references.
But sometimes you need to refer to other blocks or mixins. In this case, you
should use the public methods of these blocks, i.e. $('.b-another').b_another_hide();
(Unfortunately, you can't easily create namespaces in jQuery, like $('.b-another').b_another.hide(),
so it is suggested to use the name of the block as a prefix to simulate namespaces: b_another_hide()).
Think about blocks as services with an API - you can talk with the service (block) only
via its specified API, and you can't directly change the inner state/workings of a block from the outside
- only ask it to do something via its API.
Sometimes, you will need to add some styles only if JavaScript is enabled in the user's browser. For this, you can use this trick:
a. Add #nojs id to the <body>
b. Add some styles only for enabled javascript:
#js .b-something { display: block }
c. Add JS code that changes body id to #js: document.body.id = "js";
Also, it is strongly recommended to use JSLint for validation of JS syntax. It helps to find errors
and mistypes in the code, and make it cleaner. Use following global settings for JSLint:
/*jslint bitwise: true, browser: true, eqeqeq: true, newcap: true, plusplus: true, regexp: false, rhino: true, undef: false, white: true */
/*global jQuery, ...other your globals... */
3. Images
If a block uses images, they should be stored here: /images/framework/block/something. E.g.:
/images/framework/block/something/bg.png
4. HTML (Views)
HTML markup for a block should be stored in /app/views/framework/block/something. Main file should be _b-something.html.haml. It can include other files from this directory, e.g., _image.html.haml, using 'render :partial => "image"' or 'render "image"'. If a block has other blocks, they should be included by render :partial. Ideally, action views (index.html.haml, edit.html.haml, etc.) should contain only Layouts and 'render's. E.g.:
app/views/session/index.html.haml:
.l-wrapper
render :partial => '/framework/block/login-form/b_login_form'
render :partial => '/framework/block/something/b_something'
app/views/framework/block/login-form/_b_login_form.html.haml
.b-login-form
- form_for :user do |f|
# Code of the form
-# JS behavior initializer for the block (if any)
= javascript_tag("jQuery('.b-login-form').b_login_form();")
Unfortunately, you can't use hyphens ('-') in names of partials. Use underscores instead.
Ideally, HTML of blocks should not directly contain the HTML of other blocks, only 'render's. It should also not contain any Layouts.
But blocks can have Mixins.
== Layouts
Rules are pretty much the same as for Blocks. But they are placed in different directories:
/stylesheets/framework/layout/something/ - stylesheets
/javascripts/framework/layout/something/ - javascript logic for the layout
/images/framework/layout/something/ - images for the layout
/app/views/framework/layout/something/ - for the HTML markup of the layout
But generally, layouts don't need JS logic, separate HTML markup, and they need images very rarely. If your layout needs many of these things, maybe it is already a block? :)
It is ok to use layout markup in action views or global layout views (e.g., app/views/layout/application.html.haml)
== Mixins
The rules are pretty much the same as for Blocks. But they are placed in different directories:
/stylesheets/framework/mixin/something/ - stylesheets
/javascripts/framework/mixin/something/ - javascript logic for the mixin
/images/framework/mixin/something/ - images for the layout
/app/views/framework/mixin/something/ - for the HTML markup of the mixin - rarely needed
Mixins should not be used as separate blocks. They should be included in another blocks to give them additional functionality. If a piece of functionality is repeated in several different places on a site, it is a good idea to move this functionality to a separate mixin and add this mixin to blocks. E.g. m-calendar adds JS calendar to any block that contains date/time selects (hides date/time selects, adds textfield input, adds calendar, adds events for calendar, hide calendar, etc).
Generally, you don't need separate HTML markup for mixins. It is simply used in other blocks.
It is recommended to add names of mixins *after* name of block. E.g.:
<div class="b-block m-popup m-carousel">
<!-- Something here -->
</div>
Or for HAML:
.b-block.m-popup.m-carousel
=# Something here...
=== Global styles
1. /stylesheets/framework/global/global.css
Contains all global styles. Number of these styles should be small. Generally, only html, body.
2. /stylesheets/framework/global/global.reset.css
Contains global reset styles, like * { margin:0;padding:0 } and others.
3. /stylesheets/framework/global/global.ie.css
Contains all global styles for IE.
=== Overall structure of the app with just one block: b-something
app/
views/
framework/
block/
something/
_b_something.html.haml
public/
stylesheets/
framework/
block/
something/
b-something.css
b-something.ie.css
application.css
application.ie.css
javascripts/
framework/
block/
something/
b-something.js
application.js
images/
framework/
block/
something/
bg.png
== FAQ
1. How are you using the "classes" var in JavaScript template?
Often, you need to make some manipulations with the DOM objects. We almost don't use ids (#blabla) by our conventions, only if this is really necessary for improving performance (if this is a bottle-neck). But mostly, we use classes. I suggest avoiding use of inline classes (because refactoring there is very often, and we often need to change the names of the classes), but use classes variable instead. E.g. use:
var classes = { some_class: 'b-something_some-class' };
$('.' + classes.some_class).hide();
instead of
$('.b-something_some-class').hide();
This way, if we change the name of the class, we won't need to look for every entry of this class in the code, but just need to change its value in the 'classes' variable.
2. What does this syntax "$.fn.b_something" mean/do? Why use it?
We add functions ('b_something()' in this example) to jQuery.fn namespace by such way. Then, we can apply this function to jQuery object, e.g.:
$('.b-something').b_something();
3. What does the function that you've assigned to it do? Can you explain line by line?
// Start anonymous function. This function will be executed immediatly
// after declaration, and we pass jQuery object to it as $. We use the function
// for making all its contents private.
(function($) {
// classes object I mentioned above...
var classes = {
some_class: 'b-something_some-class'
// Other classes
};
// Add b_something() function to jQuery.fn namespace (jQuery.fn functions can be applied to jQuery objects).
$.fn.b_something = function(options) {
// Add/override default options by user-defined options. In Ruby, this string could be looked like:
// opts = $.fn.b_something.defaults.merge(options)
var opts = $.extend({}, $.fn.b_something.defaults, options);
// 'this' is equal to $('b-something') here. If there are 2 DOM elements with class 'b-something', 'this'
// will contain two DOM elements in $('b-something') object. So, we need to iterate over every DOM element:
return this.each(function() {
// 'this' is equal to DOM element with class 'b-something' here. But this is more convenient to work with
// jQuery objects, so we create a jQuery object with only one DOM element with 'b-something' class.
var self = $(this);
// Code of the JS logic
});
};
// Default options that will be passed to b_something function.
$.fn.b_something.defaults = {
// Defaults
};
// Execute described function immediatly after declaration and pass jQuery object to it.
})(jQuery);
This document was heavily inspired by a presentation by Vitaly Harisov and by the articles on his site.
(Warning - Russian language!)
http://vitaly.harisov.name/article/independent-blocks.html - About independent blocks
http://company.yandex.ru/experience/css-framework/theory.html - Presentation (text)
http://narod.ru/disk/5655929000/V_Harisov_CSS_Framework_svoimi_rukami_teoriya.avi.html - Presentation (video)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment