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

Tuesday, January 22, 2008

My ImageFlow with Lightbox packaged sample

Per a request from Clemens to my last post, I've packaged my revised version of ImageFlow and Lightbox2. In addition, I've made my sample application issue an Ajax request to get the list of images to demonstrate how you can dynamically build the photo album. Here is the code:

// sample application
var MyApp = function() {
 var imgTemplate = new Template('<img alt="#{caption}" longdesc="javascript:LightBoxLite.displayImage(\'#{filename}\')" src="reflect.php?img=#{filename}" />');

    function initializeAlbum(response) {
        var imageList;
        var imgHTML = '';

        try {
            imageList = response.responseJSON; 
        } catch (e) {
            console.error(e, "\n", response.responseText);
        }

        // IE bug : for all other browsers an imageList.each() works great here,
        // however IE doesn't like it so we revert to a for loop
        for (var i = 0; i < imageList.length; i++) {
            var filename = imageList[i];
            // IE bug : IE computes an incorrect value for imageList.length resulting 
            // in undefined filenames in the loop, check for that
            if (filename) {
                // for simplicity, extract the base filename for the caption.  
                // Caption could also be defined for each image in the JSON response data.
                var caption = filename.split('/').pop().split('.')[0].gsub("%20", " ");
                imgHTML += imgTemplate.evaluate({filename : filename, caption : caption});
            }
        }

        $('images').innerHTML = imgHTML;
        // IE bug : IE needs a moment to let the innerHTML change register before proceeding
        // so we add a small timeout to let it catch its breath
        setTimeout(ImageFlow.initialize, 10);
    }

    return {
        loadAlbum : function(albumName) {
            $('startButton').hide();
            $('photoAlbum').show();
            var myAjax = new Ajax.Request('imageList.php?albumName=' + albumName, {
                method: 'get',
                onSuccess: initializeAlbum
                });
        }
    };

}();
The initial button you see in the sample application, invokes the loadAlbum() function when clicked as shown:
<input type="button" value="Click here to load photo album" onclick="MyApp.loadAlbum('FlickrEyeCandy');"></input>
Download the packaged sample application shown below. Update January 23, 3pm EST: It was reported that the sample application didn't work in IE. This was due to some deficiencies in IE which I have accounted for now. I have pointed them out in the code with the comments beginning with IE bug

Friday, January 18, 2008

ImageFlow with Lightbox Lite

I like the idea of using ImageFlow with Lightbox2, however, I don't like that it required a tweaked version of ImageFlow, nor that I then had to modify my image list to wrap the Lightbox2 elements around them. Additionally, Lightbox2 adds a lot of functionality that's not being using in this case (all the next/previous image controls, image caching, etc), and it also wants to initialize itself during the window.onload event which I don't want in my Ajax apps.

So, wrote my own Lightbox Lite. Well, technically, I extracted my own Lightbox Lite from the Lightbox2 code with a few modifications.

To begin with, I added this block of HTML to my web page. The Lightbox2 code constructs this dynamically in its initialize function, but I don't see the advantage of that vs just hard coding it into the page to begin with.


<div id="photo" style="display:none">
<div id="lightbox">
<div id="outerImageContainer">
<div id="imageContainer">
<img id="lightboxImage" style="display:none"></img>
<div id="imageloading">
<img src="images/loadingImage.gif">
</div>
</div>
</div>
<div id="imageDataContainer" style="display:none">
<div id="imageData">
<div id="imageDetails">
<span id="imagecaption"></span>
</div>
<div id="imageBottomNav" onclick="LightBoxLite.reset();" style="float:right" >
<img src="images/close-button.png" alt="Close" title="Close">
</div>
</div>
</div>
</div>
</div>

Then I added the relevant CSS rules to my stylesheet:

#photo {
z-index: 51;
position: absolute;
top: 5px;
left: 1px;
width: 100%;
}

#outerImageContainer {
background-color: white;
width: 250px;
height: 250px;
margin: 0 auto;
}

#imageContainer {
padding-top: 10px;
}

#imageloading {
position: absolute;
top: 40%;
left: 47.5%;
}

#imageDataContainer {
font: 10px Verdana, Helvetica, sans-serif;
background-color: white;
margin: 0 auto;
line-height: 1.4em;
overflow: auto;
width: 100%;
padding-bottom: 5px;
}

#imageData {
padding: 0 10px;
color: #666;
}

#imageData #imageDetails {
width: 80%;
float: left;
text-align: left;
}

#imageData #imageCaption {
font-weight: bold;
}

Next I added the Lightbox Lite code to my JavaScript (note, you still need prototype.js and script.aculo.us):

LightBoxLite = function() {

var borderSize = 10;
var resizeDuration = 0.6;

/**
* resizeImageContainer
*
* @param {Number} desired width
* @param {Number} desired height
*/
function resizeImageContainer( imgWidth, imgHeight) {

// get current width and height
var widthCurrent = $('outerImageContainer').getWidth();
var heightCurrent = $('outerImageContainer').getHeight();

// get new width and height
var widthNew = (imgWidth + (borderSize * 2));
var heightNew = (imgHeight + (borderSize * 2));

// scalars based on change from old to new
var xScale = ( widthNew / widthCurrent) * 100;
var yScale = ( heightNew / heightCurrent) * 100;

// calculate size difference between new and old image, and resize if necessary
var wDiff = widthCurrent - widthNew;
var hDiff = heightCurrent - heightNew;

if (!( hDiff === 0)) {
new Effect.Scale('outerImageContainer', yScale, {scaleX: false, duration: resizeDuration, queue: 'front'});
}
if (!( wDiff === 0)) {
new Effect.Scale('outerImageContainer', xScale, {scaleY: false, delay: resizeDuration, duration: resizeDuration});
}

$('imageDataContainer').style.width = widthNew + "px";

showImage();
}

/**
* updateDetails
*
*/
function updateDetails() {
var caption = $('lightboxImage').src.split('/').pop().split('.')[0].gsub("%20", " ");
$('imagecaption').innerHTML = caption;

new Effect.Parallel(
[ new Effect.SlideDown( 'imageDataContainer', { sync: true, duration: resizeDuration, from: 0.0, to: 1.0 }),
new Effect.Appear('imageDataContainer', { sync: true, duration: resizeDuration }) ]
);
}

/**
* showImage
* Display image and begin preloading neighbors.
*/
function showImage() {
$('imageloading').hide();
new Effect.Appear('lightboxImage', { duration: resizeDuration, queue: 'end', afterFinish: updateDetails });
}

function checkForPreloadComplete(imgPreloader) {
if (imgPreloader.complete) {
$('lightboxImage').src = imgPreloader.src;
resizeImageContainer(imgPreloader.width, imgPreloader.height);
} else {
setTimeout(checkForPreloadComplete.bind(this, imgPreloader), 100);
}
}
return {
/**
* displayImage
* display an image in a lightbox
*
* @param {String} URL of image
*/
displayImage : function(imageUrl) {
$('imageloading').show();
$('lightboxImage').hide()
$('imageDataContainer').hide()
$('photo').show();

var imgPreloader = new Image();
imgPreloader.src = imageUrl;

// once image is preloaded, resize image container
setTimeout(checkForPreloadComplete.bind(this, imgPreloader), 100);
},

reset : function() {
$('photo').hide();
$('imageloading').show();
$('lightboxImage').hide()
$('imageDataContainer').hide()
$('lightboxImage').src = "";
$('outerImageContainer').style.width = "250px";
$('outerImageContainer').style.height = "250px";


}
};
}();

The only alteration you have to do to your ImageFlow images is to add a call to the LightBoxLite.displayImage function in the longdesc attribute of your images, which should currently have the address of the original image to display when you click on it in the image flow. So, instead of:

<img src="reflect.php?img=myimage.jpg" longdesc="myimage.jpg" alt="myimage" />

It becomes:

<img src="reflect.php?img=myimage.jpg" longdesc="javascript:LightBoxLite.displayImage('myimage.jpg')" alt="myimage" />


Here is an example of it being used.

Update January 20: I tweaked a couple CSS rules to correctly center the loading indicator for Internet Explorer.

Update January 23: I received a request to package my modifications. See my new blog entry where I have done that in addition to demonstrating how to add Ajax to receive the image list.

Thursday, January 17, 2008

ImageFlow (improved)

Finn Rudolph has created a very nice "cover flow" type of control in JavaScript called ImageFlow (which is an improvement of Michael L. Perry's Cover flow).

It's a very nice package and quite easy to use! However, it currently has a few code shortcomings. My primary issues with it was that it defined everything in a global scope (with common names that easily clash with other functions) and that it assumes the photos are in a static page and thus initializes itself when the web page loads. The later issue was the biggest thing I needed to fix since I wanted to use it in an Ajax application where I'm getting the images as a result of an XMLHttpRequest.


  • I added an "ImageFlow" scoping around the whole package so that all the variables and particularly the functions aren't global to my whole application (and potentially clashing).

  • I added "var" declarations for many of the variables which didn't specify "var" so that they would not become fully global variables, but only global to the ImageFlow scope.

  • I changed the "onload" function to be an "initialize()" function so that I could execute it when my page was ready

  • I made all the variables and functions totally private to ImageFlow with the only publicly visible function being "intialize()"

  • Since the images in the image flow div are now loaded sometime after the page is loaded, I added a check to the initialize function determine when all images are really loaded (otherwise some would be sized wrong).


Now, whenever you're ready for the ImageFlow to be displayed, just call "ImageFlow.initialize()"

If you're interested in these updates, you can download my altered imageflow.js. I have also submitted them to Finn, so hopefully they will be adopted into the official version.

NOTE: The file was updated on January 18, 10:00am EST after running it through JSLint -- which I should have done before posting it originally.

Update January 18: I've added a new article describing the addition of a scaled down version of Lightbox2 without modifying ImageFlow.

Friday, January 11, 2008