Friday, February 22, 2008

busyProcess: a visual indicator for long JavaScript tasks

Some time ago I wrote an article to demonstrate a simple method of displaying a processing/loading indicator for Ajax requests using prototype. That works great for Ajax requests, but let's assume you have some CPU intensive task that will be processed on the client side in JavaScript. This task will take several seconds to complete and you want to add some sort of visual indicator to the page while it's working so the user knows something is happening. enter the busyProcess function.

You could code your particular JavaScript task to display a processing indicator itself and remove it when it's done, however there are two problems you'll encounter. First, you'll probably never see the processing indicator since your task will immediately do its thing and not give control back to the browser in order to display the indicator before the task finishes and your code removed the indicator. Second, you'll end up adding the same code over and over around each function that may take a while to complete (DRY - Do not Repeat Yourself!).

busyProcess handles both of these situations. It is a flexible wrapper for any function passed in to it so that it can be used for any task, and it defers the execution of the function in order to allow the browser to render the visual indicator.

Example

Click this to be busy for 3 seconds.

Get to the code already!


/**
* busyProcess
*
* Add a busy indicator over an element while running the function it invokes
* @param {Object} element clicked on to invoke the task
* @param {Function} function to invoke after adding visual indicator
*/
function busyProcess(element, func) {
var busyIndicator = new Element("div", {id: "busy"});
busyIndicator.setStyle({zIndex: "100",
position: "absolute",
fontWeight:"bold",
height: "16px"});
Position.clone($(element), busyIndicator, {setWidth: false, setHeight: false});
busyIndicator.innerHTML = ' Processing...';
document.body.appendChild(busyIndicator);

// function needs to be deferred in order for the browser to render the
// busy indicator, but we need to wrap it in order to remove the busy indicator
// when it's done
func = func.wrap(
function(proceed) {
proceed();
busyIndicator.remove();
});
func.defer();
}



Ok, so how does it work?

The function takes in two arguments. The first is the element clicked on. This is used as the location of the popup processing indicator as the item clicked on is what invoked the action for the user. Since the prototype $() function is used, this can be an element id as well. The second argument is the function to invoke.

busyProcess starts by building the processing indicator. This can be customized to your preferences. Just be sure the indicator has a z-index greater than anything other layers on the screen, and that is has absolute positioning. I then utilize the prototype Position.clone to position the indicator over the element clicked on.

Now we get to the more interesting part. The indicator needs to go away when the task is done. But how do we know when it's done? You could make the task issue a custom event and register an event listener here to catch it in order to remove the processing indicator. However, then any function you use busyProcess with will need to issue that event when it's done. That could get messy, and easy to forget. So, instead we wrap the original function to have it remove the processing indicator when it's done.

So, how do you use this?

For a simple example, let's assume we want to sort some large table by different columns when the user clicks on little triangle icons indicating ascending or descending.
For simplicity I'm just going to add the code inline on an onclick attribute in the HTML. As a general practice I usually try to register event handlers in my JavaScript code so that the HTML has no JavaScript in it.


<td id="zipcode">Zip Code
<img src="images/sort-asc.gif" alt="ascending sort icon"
onclick="busyProcess(this, function() {sortTableBy(this.up('td').identify());}.bind(this));" />
</td>


I intentionally made that a little more complicated than it had to be just to demonstrate that you can use inline anonymous functions as well.

Optional (but useful) addition

In order to give a more obvious indication that work is being done and to prevent the user from clicking on anything else until it's done you may want to either darken or lighten the rest of the page. To do this, add another layer at the beginning of the busyProcess function like:

var lightenScreen = new Element("div", {id: "lightenScreen",
'class': "lightenBackground"});
document.body.appendChild(lightenScreen);

where the lightenBackground class is defined in CSS as:

.lightenBackground {
background-color: white;
opacity: 0.5; /* Safari, Opera */
-moz-opacity: 0.50; /* FireFox */
filter: alpha(opacity = 50); /* IE */
z-index: 20;
height: 100%;
width: 100%;
background-repeat: repeat;
position: fixed;
top: 0px;
left: 0px;
cursor: wait;
}

or the corresponding darkening version:

.darkenBackground {
background-color: black;
opacity: 0.2; /* Safari, Opera */
-moz-opacity: 0.20; /* FireFox */
filter: alpha(opacity = 20); /* IE */
z-index: 20;
height: 100%;
width: 100%;
background-repeat: repeat;
position: fixed;
top: 0px;
left: 0px;
cursor: wait;
}

Just be sure to remove this layer as part of the wrap by adding the line: lightenScreen.remove();

Friday, February 08, 2008

acts_as_conference - day 1

I attended the acts_as_conference conference on Ruby on Rails. It was put on by Rails for All whose founder, Robert Dempsey had previously given a free class on Ruby on Rails in Tampa, where I live, for free which was my first real exposure to RoR, and how I learned about this conference.

The conference was attended by around 150 people. There were some technical glitches, particularly with WiFi access for everyone, but overall it ran smoothly.

Here are the topics covered with a brief summary of each:

Advanced DSLs in Ruby - Neal Ford

A DSL is a domain specific languages. Basically this is about building fluent (readable) interfaces and improve abstraction by eliminating noise.

So you can type:

recipe = Recipe.new "Spicy Bread"
recipe.add 200.grams.of Flour
recipe.add 1.lb.of Nutmeg

instead of something like:

recipe = Recipe.new("Spicy Bread")
flour = new Ingredient("Flour")
flour.setMeasurement("grams")
flour.setAmount(200)
recipe.add(flour)
nutmeg = new Ingredient("Nutmeg")
nutmeg.setMeasurement("lb")
nutmeg.setAmount(1)
recipe.add(nutmeg)

This works by using a few techniques. First extending the Numeric class to allow the numbers to converted based on the measurement type to a common measurement in grams, and add an of function to build a new Ingredient object

class Numeric
def gram
self
end
alias_method :grams, :gram

def pound
self * 453.59237
end
alias_method :lb, :pound
alias_method :lbs, :pound
alias_method :pounds, :pound

def of ingredient
if ingredient.kind_of String?
ingredient = new Ingredient(ingredient)
end
ingredient.quantity = self
ingredient
end
end

The reason you can create the Ingredient without surrounding the Ingredient name with quotes is by type transmogrification which uses of the const_missing construct:

class Object
def self.const_missing(sym)
eval "Ingredient.new(sym.to_s)"
end
end

Neal added some additional techniques like using bubble method in order to build a nutrition profile for the recipe as you build the recipe.
Overall, the presentation was very interesting to see how you can manipulate Ruby to make ever more readable code. However, in general I don't think it's worth the effort or the confusion for new developers trying to figure out how the code even works.

Working with others: Best Practices for Rails Teams - Luke Francl

Luke discussed some of the issues that can arise in a team environment, specifically for Rails. He had a nice handout which unfortunately isn't available in softcopy form. Here are the bullet points.

  • Migrations - Migrations have a tendency to stop working (what he calls migration decay). So, use the schema.rb as the authoritative source for your database schema. When creating the database on another system run rake db:schema:load instead of running the migrations.
  • Seed Data - Loading seed data in your migrations can break as the models change and fixtures are for test data. Luke recommends using db-populate to populate seed data along with ActiveRecord::Base.create_or_update

    def self.create_or_update(options = {})
    id = options.delete(:id)
    record = find_by_id(id) || new
    record.id = id
    record.attributes = options
    record.save!
    record
    end

  • 3rd party code - vendor everything using gemsonrails
  • Security - HTML escape everything in your views with h. There are several XSS plugins that can be used as well. Mass assignments (ex. LineItem.new(params[:line_item])) can be dangerous, be sure to protect your attributes with:

    • :attr_protected - attributes you can't write to (via mass parameter assignment)

    • :attr_accessible - attributes you can write to



  • Source control - The source code is the life of your project. Be sure to use source control management (SCM). Commit atomic changes, not batches of changes with informative messages.

  • Bug tracking - Use a bug tracker that has some workflow for various states of completion as well as email integration to inform interested parties and SCM integration to link code fixes to bugs.

  • Continuous integration - Tis ties everything together. Verifies that new code doesn't break anything else right away, ensures all libraries and code packages are available, etc.



Rails on AIR - Peter Armstrong


This was mostly about using Flex and using Flex Builder (Eclipse). Flex is way to build Flash applications without knowing Flash and AIR allows your Flash application to run as a stand alone desktop applications. Frankly, I wasn't too interested in this. It seems like a lot of work for not much gain. Yes, Flash is installed in like 99% of browsers, however there are drawbacks to using it in web apps. For example, you site with not be indexable be search engines which can reduce the traffic to your site, and I've been able to accomplish most everything I would need to use Flash for with HTML/CSS/JavaScript.

Keynote - Dan Benjamin


Dan provided some tips for developing good software development and interface design:

  • As Simple As Possible, But No Simpler
  • Focus on Creating a Great User Experience
  • Anticipate User Actions
  • Think Like your Users
  • You're Probably Not as Good of a Designer as you Think
  • Develop for One Scenario, Not for Ten
  • Good Code Does Not Impress Users
  • Don't Release a Beta
  • Apologize (for any problem)
  • Just Ship It
"simplicity is the key to happiness in the modern world"


That's all for day 1. Watch for day two which has these topics:
  • JRuby - Charles Nutter
  • Shining a Light on the Dark Magic of ActiveRecord - Anthony Eden
  • Smarticus University: BDD With RSpec - Bryan Liles
  • Adding Media to Your Rails Application - Dave Naffis and Josh Owens
  • Lessons from the Trenches – Learning from the Rails Bootcamp - Charles Brian Quinn
  • Keynote - Obie Fernandez