About Us

Bulletpoint StarImulus® is a technology focused design + interactive agency.

In addition to our client services we also have a few products in the works. Our office is always filled with chatter and this blog is an outlet for our creative energy, rants and ideas.

Podium

Stacks!
Imulus built a task management solution that finally works for teams. It's a life saver, learn more at usestacks.com.

Featured Project

Author: Casey ()

Dec9

Reserved usernames to avoid vanity URL collision

As we continue to add support for more languages on our tech support management application, Support Details, one thing we’ve considered is how vanity URLs will impact the introduction of new features. In the short future we plan to add support for custom accounts, so it made sense to compile a list of usernames we’d reserve out of the gate to avoid collision in the future.

Here’s our running list:


reserved_usernames = [
	# Companies we'd love to have use our service
	# so we'll reserve them to be safe
	'supportdetails',
	'support-details',
	'stacks',
	'imulus',
	'github',
	'twitter',
	'facebook',
	'google',
	'apple',

	# Generic reserved words
	'about',
	'account',
	'activate',
	'add',
	'admin',
	'administrator',
	'api',
	'app',
	'apps',
	'archive',
	'archives',
	'auth',
	'blog',
	'cache',
	'cancel',
	'careers',
	'cart',
	'changelog',
	'checkout',
	'codereview',
	'compare',
	'config',
	'configuration',
	'connect',
	'contact',
	'create',
	'delete',
	'direct_messages',
	'documentation',
	'download',
	'downloads',
	'edit',
	'email',
	'employment',
	'enterprise',
	'faq',
	'favorites',
	'feed',
	'feedback',
	'feeds',
	'fleet',
	'fleets',
	'follow',
	'followers',
	'following',
	'friend',
	'friends',
	'gist',
	'group',
	'groups',
	'help',
	'home',
	'hosting',
	'hostmaster',
	'idea',
	'ideas',
	'index',
	'info',
	'invitations',
	'invite',
	'is',
	'it',
	'job',
	'jobs',
	'json',
	'language',
	'languages',
	'lists',
	'login',
	'logout',
	'logs',
	'mail',
	'map',
	'maps',
	'mine',
	'mis',
	'news',
	'oauth',
	'oauth_clients',
	'offers',
	'openid',
	'order',
	'orders',
	'organizations',
	'plans',
	'popular',
	'post',
	'postmaster',
	'privacy',
	'projects',
	'put',
	'recruitment',
	'register',
	'remove',
	'replies',
	'root',
	'rss',
	'sales',
	'save',
	'search',
	'security',
	'sessions',
	'settings',
	'shop',
	'signup',
	'sitemap',
	'ssl',
	'ssladmin',
	'ssladministrator',
	'sslwebmaster',
	'status',
	'stories',
	'styleguide',
	'subscribe',
	'subscriptions',
	'support',
	'sysadmin',
	'sysadministrator',
	'terms',
	'tour',
	'translations',
	'trends',
	'unfollow',
	'unsubscribe',
	'update',
	'url',
	'user',
	'weather',
	'webmaster',
	'widget',
	'widgets',
	'wiki',
	'ww',
	'www',
	'wwww',
	'xfn',
	'xml',
	'xmpp',
	'yaml',
	'yml',

	# Top 50 languages by speaking population
	'chinese',
	'mandarin',
	'spanish',
	'english',
	'bengali',
	'hindi',
	'portuguese',
	'russian',
	'japanese',
	'german',
	'wu',
	'javanese',
	'korean',
	'french',
	'vietnamese',
	'telugu',
	'chinese',
	'marathi',
	'tamil',
	'turkish',
	'urdu',
	'min-nan',
	'jinyu',
	'gujarati',
	'polish',
	'arabic',
	'ukrainian',
	'italian',
	'xiang',
	'malayalam',
	'hakka',
	'kannada',
	'oriya',
	'panjabi',
	'sunda',
	'panjabi',
	'romanian',
	'bhojpuri',
	'azerbaijani',
	'farsi',
	'maithili',
	'hausa',
	'arabic',
	'burmese',
	'serbo-croatian',
	'gan',
	'awadhi',
	'thai',
	'dutch',
	'yoruba',
	'sindhi',

	# Country TLDs
	'ac',  # Ascension Island
	'ad',  # Andorra
	'ae',  # United Arab Emirates
	'af',  # Afghanistan
	'ag',  # Antigua and Barbuda
	'ai',  # Anguilla
	'al',  # Albania
	'am',  # Armenia
	'an',  # Netherlands Antilles
	'ao',  # Angola
	'aq',  # Antarctica
	'ar',  # Argentina
	'as',  # American Samoa
	'at',  # Austria
	'au',  # Australia
	'aw',  # Aruba
	'ax',  # and
	'az',  # Azerbaijan
	'ba',  # Bosnia and Herzegovina
	'bb',  # Barbados
	'bd',  # Bangladesh
	'be',  # Belgium
	'bf',  # Burkina Faso
	'bg',  # Bulgaria
	'bh',  # Bahrain
	'bi',  # Burundi
	'bj',  # Benin
	'bm',  # Bermuda
	'bn',  # Brunei Darussalam
	'bo',  # Bolivia
	'br',  # Brazil
	'bs',  # Bahamas
	'bt',  # Bhutan
	'bv',  # Bouvet Island
	'bw',  # Botswana
	'by',  # Belarus
	'bz',  # Belize
	'ca',  # Canada
	'cc',  # Cocos (Keeling) Islands
	'cd',  # Democratic Republic of the Congo
	'cf',  # Central African Republic
	'cg',  # Republic of the Congo
	'ch',  # Switzerland
	'ci',  # Côte d'Ivoire
	'ck',  # Cook Islands
	'cl',  # Chile
	'cm',  # Cameroon
	'cn',  # People's Republic of China
	'co',  # Colombia
	'cr',  # Costa Rica
	'cs',  # Czechoslovakia
	'cu',  # Cuba
	'cv',  # Cape Verde
	'cx',  # Christmas Island
	'cy',  # Cyprus
	'cz',  # Czech Republic
	'dd',  # East Germany
	'de',  # Germany
	'dj',  # Djibouti
	'dk',  # Denmark
	'dm',  # Dominica
	'do',  # Dominican Republic
	'dz',  # Algeria
	'ec',  # Ecuador
	'ee',  # Estonia
	'eg',  # Egypt
	'eh',  # Western Sahara
	'er',  # Eritrea
	'es',  # Spain
	'et',  # Ethiopia
	'eu',  # European Union
	'fi',  # Finland
	'fj',  # Fiji
	'fk',  # Falkland Islands
	'fm',  # Federated States of Micronesia
	'fo',  # Faroe Islands
	'fr',  # France
	'ga',  # Gabon
	'gb',  # United Kingdom
	'gd',  # Grenada
	'ge',  # Georgia
	'gf',  # French Guiana
	'gg',  # Guernsey
	'gh',  # Ghana
	'gi',  # Gibraltar
	'gl',  # Greenland
	'gm',  # The Gambia
	'gn',  # Guinea
	'gp',  # Guadeloupe
	'gq',  # Equatorial Guinea
	'gr',  # Greece
	'gs',  # South Georgia and the South Sandwich Islands
	'gt',  # Guatemala
	'gu',  # Guam
	'gw',  # Guinea-Bissau
	'gy',  # Guyana
	'hk',  # Hong Kong
	'hm',  # Heard Island and McDonald Islands
	'hn',  # Honduras
	'hr',  # Croatia
	'ht',  # Haiti
	'hu',  # Hungary
	'id',  # Indonesia
	'ie',  # Republic of Ireland  Northern Ireland
	'il',  # Israel
	'im',  # Isle of Man
	'in',  # India
	'io',  # British Indian Ocean Territory
	'iq',  # Iraq
	'ir',  # Iran
	'is',  # Iceland
	'it',  # Italy
	'je',  # Jersey
	'jm',  # Jamaica
	'jo',  # Jordan
	'jp',  # Japan
	'ke',  # Kenya
	'kg',  # Kyrgyzstan
	'kh',  # Cambodia
	'ki',  # Kiribati
	'km',  # Comoros
	'kn',  # Saint Kitts and Nevis
	'kp',  # Democratic People's Republic of Korea
	'kr',  # Republic of Korea
	'kw',  # Kuwait
	'ky',  # Cayman Islands
	'kz',  # Kazakhstan
	'la',  # Laos
	'lb',  # Lebanon
	'lc',  # Saint Lucia
	'li',  # Liechtenstein
	'lk',  # Sri Lanka
	'lr',  # Liberia
	'ls',  # Lesotho
	'lt',  # Lithuania
	'lu',  # Luxembourg
	'lv',  # Latvia
	'ly',  # Libya
	'ma',  # Morocco
	'mc',  # Monaco
	'md',  # Moldova
	'me',  # Montenegro
	'mg',  # Madagascar
	'mh',  # Marshall Islands
	'mk',  # Republic of Macedonia
	'ml',  # Mali
	'mm',  # Myanmar
	'mn',  # Mongolia
	'mo',  # Macau
	'mp',  # Northern Mariana Islands
	'mq',  # Martinique
	'mr',  # Mauritania
	'ms',  # Montserrat
	'mt',  # Malta
	'mu',  # Mauritius
	'mv',  # Maldives
	'mw',  # Malawi
	'mx',  # Mexico
	'my',  # Malaysia
	'mz',  # Mozambique
	'na',  # Namibia
	'nc',  # New Caledonia
	'ne',  # Niger
	'nf',  # Norfolk Island
	'ng',  # Nigeria
	'ni',  # Nicaragua
	'nl',  # Netherlands
	'no',  # Norway
	'np',  # Nepal
	'nr',  # Nauru
	'nu',  # Niue
	'nz',  # New Zealand
	'om',  # Oman
	'pa',  # Panama
	'pe',  # Peru
	'pf',  # French Polynesia
	'pg',  # Papua New Guinea
	'ph',  # Philippines
	'pk',  # Pakistan
	'pl',  # Poland
	'pm',  # Saint-Pierre and Miquelon
	'pn',  # Pitcairn Islands
	'pr',  # Puerto Rico
	'ps',  # Palestinian territories
	'pt',  # Portugal
	'pw',  # Palau
	'py',  # Paraguay
	'qa',  # Qatar
	're',  # Réunion
	'ro',  # Romania
	'rs',  # Serbia
	'ru',  # Russia
	'rw',  # Rwanda
	'sa',  # Saudi Arabia
	'sb',  # Solomon Islands
	'sc',  # Seychelles
	'sd',  # Sudan
	'se',  # Sweden
	'sg',  # Singapore
	'sh',  # Saint Helena
	'si',  # Slovenia
	'sj',  # Svalbard and  Jan Mayen Islands
	'sk',  # Slovakia
	'sl',  # Sierra Leone
	'sm',  # San Marino
	'sn',  # Senegal
	'so',  # Somalia
	'sr',  # Suriname
	'ss',  # South Sudan
	'st',  # São Tomé and Príncipe
	'su',  # Soviet Union
	'sv',  # El Salvador
	'sy',  # Syria
	'sz',  # Swaziland
	'tc',  # Turks and Caicos Islands
	'td',  # Chad
	'tf',  # French Southern and Antarctic Lands
	'tg',  # Togo
	'th',  # Thailand
	'tj',  # Tajikistan
	'tk',  # Tokelau
	'tl',  # East Timor
	'tm',  # Turkmenistan
	'tn',  # Tunisia
	'to',  # Tonga
	'tp',  # East Timor
	'tr',  # Turkey
	'tt',  # Trinidad and Tobago
	'tv',  # Tuvalu
	'tw',  # Republic of China (Taiwan)
	'tz',  # Tanzania
	'ua',  # Ukraine
	'ug',  # Uganda
	'uk',  # United Kingdom
	'us',  # United States of America
	'uy',  # Uruguay
	'uz',  # Uzbekistan
	'va',  # Vatican City
	'vc',  # Saint Vincent and the Grenadines
	've',  # Venezuela
	'vg',  # British Virgin Islands
	'vi',  # United States Virgin Islands
	'vn',  # Vietnam
	'vu',  # Vanuatu
	'wf',  # Wallis and Futuna
	'ws',  # Samoa
	'ye',  # Yemen
	'yt',  # Mayotte
	'yu',  # Yugoslavia
	'za',  # South Africa
	'zm',  # Zambia
	'zw'   # Zimbabwe
]

It is a merged list of the recommendations from this Quora discussion, this list of country TLDs and
this list of languages

How to Contribute:
Gist: https://gist.github.com/1453705

Dec1

Powder: A Backbone-powered iPad webapp

Powder screenshot

Winter is upon us, and like many folks in Colorado we’re stoked to hit the mountains. Given the recent snowfall and our experimentation with HTML5 localStorage, it seemed natural to push it a bit further and do something fun.

Enter Powder, an HTML5 webapp for tracking the snow conditions at your favorite ski resorts.

Powder is an experiment using backbone.js to power an iPad webapp. On the client, Powder uses the browser’s native HTML5 localStorage mechanism to save the users’s preferences, the list of resorts the user chooses to track. On the server, Powder uses Sinatra to fetch and parse various snow/ski reports from http://onthesnow.com/ then serve it up as JSON to the browser.

The Stack

Powder is free and open source, hosted on GitHub. We’d love to have you join us in making Powder better. Feel free to fork the project and help us make a kickass iPad webapp.

Nov28

An Interesting Challenge

Last week we had a client call us with an interesting problem. They have a tradeshow kiosk they’d like to use to capture form data from users and collect it at the end of each day.

The challenge: the kiosk doesn’t have an internet connection and can’t run a webserver. It can display webpages, but only through an IE8 iframe.

If the kiosk had a net connection we’d build a static form to post the data to a remote webservice. But that’s out. If the kiosk could run a webserver we’d build a simple webapp to handle the data locally. But that’s out, too.

We decided to solve the problem by saving the data to localStorage. localStorage is a key-value store built into most modern browsers, and a perfect mechanism for persisting simple form data. It has a minimal JavaScript API (setters and getters) via the window’s localStorage object that makes it really easy to work with.

Here’s how we did it:

We started by setting up a simple Storage class to interface with localStorage. This isn’t entirely necessary, but makes it easier to save and retrieve the data without having to parse/stringify on every transaction. (Yes, we love CoffeeScript.)


class Storage

  constructor: (@name)->
    @data = []

    # Check to see if the object already
    # exists in localStorage
    # If it does, grab the value
    # Otherwise, create a key in localStorage
    # with an empty array of data
    if localStorage[@name]?
      do @fetch
    else
      # We need to stringify the data as JSON
      # because it is stored as a string
      localStorage.setItem(@name, JSON.stringify(@data))

  # Pull the data out of localStorage and populate
  # the instance's @data array property
  # We need to parse the data as JSON
  # because it is stored as a string
  fetch: ->
    @data = JSON.parse(localStorage.getItem(@name))

  # Add an item to the instance's @data array and
  # re-save to localStorage
  add: (data) ->
    @data.push data
    localStorage.setItem(@name, JSON.stringify(@data))

Next, we set up a Form class to observe the form on the page and do some simple validation before finally committing it to a storage instance.


class Form

  constructor: (@$form, name)->
    @storage  = new Storage(name)
    @$fields  = $('input, textarea, select', @$form)

    @$form.submit (event) =>
      event.preventDefault()
      if @validate()
        do @save
        do @clear

  save: ->
    data = {}

    @$fields.each ->
      name  = $(this).attr('name')
      value = $(this).val()
      data[name] = value

    @storage.add data

  clear: ->
    @$fields.each ->
      $field = $(this)
      type = $field.attr('type')
      $field.val('') if type is 'text'

  validate: ->
    data = {}
    valid = true

    # Simple 'required' validation
    # Loop over each of the form fields
    # If the field is required, then check to see if it is blank
    @$fields.each ->
      $field = $(this)
      value = $field.val()
      required = $field.hasClass('required')
      if required and value is ''
        valid = false
        $field.addClass('invalid')
      else
        $field.removeClass('invalid')

    return valid

The client needed a means of retrieving the data, so we set up way to grab all of the data and render out a table with each of the entries.


class ResultsTable
  constructor: (@$table, name) ->
    @storage  = new Storage(name)
    @results = @storage.data
    $head_tr = $('<tr />')

    for field, value of @results[0]
      $head_tr.append $('<td />').text(field)

    $('thead', @$table).append $head_tr

    for result in @results
      $tr = $('<tr />')
      $tr.append $('<td />').text(value) for field, value of result
      $('tbody', @$table).append $tr

Finally, an example of how to pull it all together.

On the form page:


<form id="user-form" action="#">
  <input type="text" name="first_name" />
  <input type="text" name="last_name" />
  <button type="submit">Submit</button>
</form>

<script type="text/javascript">
  $(document).ready(function(){
    new Form($('form#user-form'), 'kiosk');
  });
</script>

On the results page:


<table id="results">
</table>

<script type="text/javascript">
  $(document).ready(function(){
    new ResultsTable($('#results'), 'kiosk');
  });
</script>

Given the constraints, we feel like this is great solution. Lucky for us, the browser environment was predictable and IE8 has support for localStorage. All in all, it was a fun experiment with localStorage.

Nov16

At Least Three States

A few weeks ago Microsoft released a video showcasing their idea of what the future of technology might look like. Some weeks later, Bret Victor wrote a rant about how unimaginative Microsoft’s vision is. The thrust of his argument is that the ubiquitous touchscreens depicted in the video are uninventive because it disregards the most fundamental and powerful tool known to man: our hands. He explains that the physical interfaces of today don’t take advantage of our hands’ ability to both feel and manipulate things.

All this talk about hands and feeling and manipulation got me thinking. Given the current state and inertia of technology, it should be our foremost goal to make our interfaces feel good.

Feeling is often the indescribable property that separates a good interface from a great one. It’s the gap between the pixels and your finger (or mouse, by extension). You hear people mention this all the time when talking about software; “I don’t know, it just feels good”. Of course they’re not actually talking about how it feels, they’re talking about how their brain perceives it would feel if it were a physical interface. They’re talking about how well it responds to their input.

Without tactile feedback, as is the case when using a touchscreen or navigating a mouse cursor around a screen, our brain can’t depend on our hands and instead relies on our eyes to proxy visual feedback. We use this information on a subconsious level to confirm our last action and plan our next one. How an interface feels is the fidelity of the visual feedback to how our brain predicts it should feel.

It’s easy to see how important visual feedback becomes to the user experience in the absense of tactile feedback. It explains why skeuomorphism has prevailed as a common user interface design approach: it’s comforting to us as users when what we see and feel in the virtual world matches our perception of the physical world. This is why buttons often look like buttons and calendars often looks like calendars.

But how an interface feels goes deeper than the pixels on the screen; it’s all about trust. As an interface designer, you enter a contract with your users. Your design implicitly guarantees the user: “When you click X, the software will do Y”. It’s the job of your interface to back this guarantee by providing the feedback necessary for the user to feel assured you kept your end of the bargain. The gap between what your UI promised and what was delivered is received by the user as negative feeling. We’ve all experienced it, that brief moment of cognitive dissonance when you beckon the software to do one thing and it appears to do something different, or worse, nothing. That’s broken trust.

I have long preached that every clickable element (links, buttons, etc) deserves at least three states: idle, hover and click. I also encourage additional states for active-hover and active-click, but they are less necessary. Even novice designers know to make the idle state of their links obvious, but it’s surprising how many neglect to provide the user proper hover and click states. The hover state signifies that the element is clickable at this moment and the click state confirms that the element was clicked, both of which are crucial to keeping up your end of the UI contract.

The very best user interfaces are those that have a two-way conversation with the user, with equal transmission amplitude from both parties. When the visual feedback provided by your interface falls short of what the user expected, you’ve subconsciously broken their trust and left them with an indescribable feeling of doubt.

Sep21

ClickTime API Wrappers for Ruby and JavaScript

For a few internal projects at the Imulus office, we’ve been collecting timesheet data from the tracking software we use, ClickTime. Lucky for us, they offer an API with full access to all of our data. Unfortunately, however, it’s a SOAP API.

SOAP can be a bear, so we’ve rolled some code to help alleviate the pain of working with it:
a Ruby wrapper and a JavaScript wrapper. Both are open source, and we hope they make your life easier. Below are some examples of how to use them.

Ruby

The Ruby wrapper (ruby-clicktime) comes in the form of a single file with one class, ClickTime.

Requirements

ruby-clicktime uses Savon, so be sure to install that first:

$ gem install savon

Usage

The ClickTime class has two methods:

ClickTime#actions will return a list of all actions available to the ClickTime SOAP API

clicktime.actions  # un_sync_all, get_sync_id, set_sync_id, ...

ClickTime#exec will execute an API call and return a hash of results

clicktime.exec :api_action, {params}

Example

First, create an instance of the ClickTime class:

require 'clicktime'

clicktime = ClickTime.new :key => 'api-key', :password => 'password'

The GetEmployeeList and GetClientList API methods, as defined in the ClickTime API docs:

<GetEmployeeList xmlns="http://clicktime.com/webservices/2.2/">
	<UserID>string</UserID>
	<ActiveOnly>boolean</ActiveOnly>
</GetEmployeeList>

<GetClientList xmlns="http://clicktime.com/webservices/2.2/">
	<UserID>string</UserID>
</GetClientList>

To execute these queries, you would call the following:

employees = clicktime.exec :get_employee_list,
    {"UserID" => 'your-user-id', "ActiveOnly" => 'true'}
clients = clicktime.exec :get_client_list, {"UserID" => 'your-user-id'}

Notice the action is passed as a snake_case symbol from the list of available actions. (clicktime.actions to list them)

JavaScript

The JavaScript wrapper is a CommonJS module, ready for use in your Node.js project via npm.

Installation

In your project:

$ npm install clicktime

And in your script:

var ClickTime = require('clicktime');

Example

First, create an instance of the ClickTime object:

var clicktime = new ClickTime({
  key: 'your-api-key',
  password: 'your-api-password'
});

The GetEmployeeList API method, as defined in the ClickTime API docs:

<GetEmployeeList xmlns="http://clicktime.com/webservices/2.2/">
  <UserID>string</UserID>
  <ActiveOnly>boolean</ActiveOnly>
</GetEmployeeList>

To execute this query you would call the following:

clicktime.exec('GetEmployeeList', {
    UserID: 'some-user-id',
    ActiveOnly: true
  }, function(response){ // your callback function
    console.log(response);
  }
);

Reference

Source code:
ruby-clicktime on GitHub
node-clicktime on GitHub

ClickTime SOAP API Documentation:
http://app.clicktime.com/documentation/webservices/2_2/WebServicesDocumentation_2_2.asp

Jun18

CoffeeScript: Namespaces, Modules and Inheritance

First, some JavaScript background.

Skip to the good stuff

Here at Imulus, we like to namespace our JavaScript by wrapping our classes in static modules. We accomplish this by simply creating an object literal with a variety of members: classes (constructors), constants, variables and static methods.

Here is a pared down example of a simple module:

var Application = {           // the module, a static wrapper

 FXSPEED : 500,               // a constant (sort of)

 user : {                     // a nested static object
   id : 123
 },

 toggle : function(){ ... },  // a static method

 Interface : function(){      // a class

   this.initialize = function(){
     this.active = true;
     this.build();
     return this;
   }

   this.build = function(){
     ...
   }

   this.doSomething = function(){
     return "did something";
   }

   return this.initialize();
 }  

}

The benefits of this technique are obvious; it allows us to have loosely-coupled, modular code that is nicely organized, available without instantiation and protects the global namespace from pollution.

Application.active // true

Application.user.id // 123

var interface = new Application.Interface()
interface.doSomething() // 'did something'

In reality, however, one mammoth object literal in a single file is a lousy way to structure your code. Instead, what we do is organize module members into separate files in a logical fashion, å la Ruby. The module and all of its static members exist in a single file named after the module, and all of the module’s classes exist in a directory named after the module with each class file named after itself without the namespace. By using this structure, files are much easier to find because the naming convention mirrors the the module namespace.

The directory structure ends up looking like this:

|-- scripts
    |-- application.js
    |-- application
        |-- controller.js
        |-- data.js
        |-- interface.js

And the module above would be structured like this:

// scripts/application.js
// creates the wrapper module and contains all static members

var Application = Application || {
  FXSPEED : 500,
  user : {
    id : 123
  },
 toggle : function(){ ... },
}
// scripts/application/interface.js

Application.Interface = function(){
  this.initialize = function(){
    this.active = true;
    this.build();
    return this;
  }

  this.build = function(){
    ...
  }

  this.doSomething = function(){
    return "did something";
  }

  return this.initialize();
}
// scripts/application/controller.js

Application.Controller = function(){
  ...
}

You get the picture. This probably looks familiar to a Rubyist. Here’s the analog:

|-- lib
    |-- application.rb
    |-- application
        |-- interface.rb
# application.rb

module Application
 FXSPEED : 500,             

 def self.user
  { 'id' => 123 }
 end

 def self.toggle
  ...
 end
end
# application/interface.rb

module Application
  class Interface

    attr_accessor :active

    def initialize
      @active = true
      build
    end

    def build
      ...
    end

    def do_something
      'did something'
    end
  end
end

Snakes on a Train

By now it’s probably evident how much we obsess over code structure, organization and namespacing. While experimenting with CoffeeScript, this has been an ongoing concern.

This is the example from the CoffeeScript demo page:

class Animal
  constructor: (@name) ->

  move: (meters) ->
    alert @name + " moved " + meters + "m."

class Snake extends Animal
  move: ->
    alert "Slithering..."
    super 5

class Horse extends Animal
  move: ->
    alert "Galloping..."
    super 45

sam = new Snake "Sammy the Python"
tom = new Horse "Tommy the Palomino"

sam.move()
tom.move()

I love that Coffee has made dealing with prototypes much easier with the ‘class’ and ‘extends’ keywords. This is great, and by itself works like a charm for singular, somewhat global classes. However, for any application with a sizeable codebase this simply isn’t practical without ending up with insane class names.

The ideal solution would be to introduce a ‘module’ keyword similar to Ruby’s.

module Zoo
  class Animal
    constructor: (@name) ->

    move: (meters) ->
      alert @name + " moved " + meters + "m."

bob = new Zoo::Animal "Bob the Bird"
bob.move()

Other people agree. But there are a number of JS limitations that prevent this from being a viable solution in CoffeeScript.

The Really Good Stuff

By its own golden rule, CoffeeScript is: “It’s just JavaScript”. So, we can namespace our CoffeeScript classes in the same manner we organize our modules in vanilla JS.

Extending on the example above (ha-ha, get it?), I’ll demonstrate how to namespace your Coffee classes in a simple, elegant way.

Focusing only on the snakes for now, lets set up our basic classes.

class Animal
  constructor: (@name) ->

  move: (meters) ->
    console.log @name + " moved " + meters + "m."

class Snake extends Animal
  move: ->
    super 5

george = new Snake "George"
george.move() // George moved 5m

Now, we want to create ‘Forest’ and ‘Savanna’ modules that have a habitat and snakes of their own.

animal-diagram

Just like JavaScript, we could wrap all of the module members in an object literal:

Forest =
  habitat: "dense highland dotted with lakes and streams"
  Snake: class extends Snake
    move: ->
      super 10

george = new Forest.Snake "George"
george.move() // George moved 5m.

But we would run into the same problem when the codebase reaches any kind of scale: enormous, single-file object literals. So let’s introduce a new function for creating module members.

module = (name) ->
  global[name] = global[name] or {}

(Yes, I am aware that this is attaching the objects to the global namespace. This is what we want. It allows us to add members to the module later.)

Now we can use the ‘module’ function like this:

module 'Forest'

Forest.habitat = "dense highland dotted with lakes and streams"

Forest.Snake = class extends Snake
 move: ->
   super 10

george = new Forest.Snake "George"
george.move() // George moved 10m.

Pretty elegant, right? Additionally, by using the ‘module’ function we can now organize our code into multiple files:

module = (name) ->
  global[name] = global[name] or {}

# src/animals.coffee
class Animal
  constructor: (@name) ->

  move: (meters) ->
    console.log @name + " moved " + meters + "m."

class Snake extends Animal
  move: (meters) ->
    super meters

# src/forest/snake.coffee
module 'Forest'

Forest.habitat = "dense highland dotted with lakes and streams"

Forest.Snake = class extends Snake
  move: (meters)->
    @distance = meters || 5
    super @distance

# src/savanna/snake.coffee
module 'Savanna'

Savanna.habitat = "grassy woodland small or widely spaced trees"

Savanna.Snake = class extends Snake
  move: (meters)->
    @distance = meters || 10
    super @distance

george = new Forest.Snake "George"
george.move() // George moved 5m

scott = new Savanna.Snake "Scott"
scott.move() // Scott moved 10m

We can also extend standalone classes with module classes:

class Python extends Savanna.Snake
  constructor: ->
    super
    console.log "I am #{@name} the python."
    console.log "I live in the savanna,
    where it is #{Savanna.habitat}."

class Cobra extends Forest.Snake
  constructor: ->
    super
    console.log "I am #{@name} the cobra."
    console.log "I live in the Forest,
      where it is #{Forest.habitat}."

john = new Python "John"
// I am John the python.
// I live in the savanna,
// where it is grassy woodland small or widely spaced trees.

bruce = new Cobra "Bruce"
// I am Bruce the cobra.
// I live in the forest,
// where it is dense highland dotted with lakes and streams.

May26

Starting Fresh

After patiently waiting for their arrival, Taylor and I received our new SSDs yesterday. Rather than migrating our data over from our old disks and potentially cheapening the solid-state experience, we both opted to start with a fresh slate. (By all accounts, he’s a glutton for reformatting his system. It’s a hobby of his.)

I was reluctant, mostly in anticipation that I’ll want to start fresh again in a few months when OS X Lion is released. Having just started on this machine a few months ago, I’m also keenly aware of how long it takes to get a system up and running and configured to my tastes.

Out of both curiosity and to make it easier on myself in the future, I decided to keep track of my installation and configuration process.

In roughly the right order: