Prototypal Inheritance in JavaScript

I was recently challenged with a problem that required the use of prototypal inheritance in JavaScript, along with the implementation of a feature that is not all there yet: the CSS3 resize property.  The challenge was this: 

1)   Create a JavaScript Class called “Cell” which creates an HTML box of a specified height(px), width(px) and color.  The box should have a method called “render” that will force the box to be drawn on the screen, or updated if already rendered.

2)   Create a JavaScript Class called "Grid" which will create a grid of a specified number of rows, columns, spacing between each grid cell. The grid should have sane defaults for each of the parameters if not specified.  The Grid should render a new "Cell" in each intersection of a row and column.  

a.     The Grid Class should have a "randomize" method that when called randomizes the color of the Cells contained inside

b.     The Grid should have a "resize" method that will grow or shrink the size of the grid as well as the number of cells contained inside based on the parameters specified

3)   Create Class called "ResizableGrid" which extends "Grid". The resizable grid should all users to grab the lower right hand corner of the grid and resize it to their liking. The grid should add / remove cells as needed to fill the allowable area. When being resized, the grid should give some visual indication as to how big the grid will be if the user were to stop. You may only use HTML, CSS, and JavaScript to complete the challenge. You may use any base library such as mootools, jquery, prototype, etc. You may NOT use any UI library such as jQueryUI, Scriptaculous or ExtJS.  You may not use any server size technologies such as php, java, python, etc.

 

Okay, moving on to the implementation of (1).  In JavaScript, namespacing is good, so I decided to create a "Challenge" namespace that contained all of the classes.  First off, the Cell class shown below.  It is simple.  Two properties, and two methods.  The two properties, CellTemplate and CurrentOptions are set to their default values when the Cell class is created.  The Init and Render methods allow for class initialization and visual display, respectively.

 

/**
* Challenge Namespace
*/
var Challenge = Challenge || {};

(function () {

/**
* Cell class
*/
Challenge.Cell = function () {

// property definitions and default initializations
//
this.CellTemplate = '<div style="width: {0}; height: {1}; background-color: {2}; margin: 0 auto;"></div>';
this.CurrentOptions = null;
};

Challenge.Cell.prototype.Init = function (height, width, color, otherOptions) {

var self = this;
var heightOption = '100px';
var widthOption = '100px';
var colorOption = 'rgb(127, 127, 127)';

if ((height != null) && (!isNaN(height))) {
heightOption = height;
}

if ((width != null) && (!isNaN(width))) {
widthOption = width
}

if (color != null) {

var colorMatchRegex = /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/;
var colorMatch = colorMatchRegex.exec(color);

if (colorMatch !== null) {
colorOption = color;
}
}

//set default options
otherOptions = $.extend({
height: heightOption,
width: widthOption,
color: colorOption
}, otherOptions || {});

self.CurrentOptions = otherOptions;
};

Challenge.Cell.prototype.Render = function() {

var cellSelector = "#cell-challenge";
var cellToRender = this.CellTemplate.replace('{0}', this.CurrentOptions.width);

cellToRender = cellToRender.replace('{1}', this.CurrentOptions.height);
cellToRender = cellToRender.replace('{2}', this.CurrentOptions.color);

$(cellSelector).html(cellToRender);
};
})();

 

The use of the Cell's prototype here takes advantage of JavaScript's inherent capability to extend objects with new methods and properties. This way, every object that inherits from the Cell class, will have access to the Init and Render methods.  The (function () { ... })(); might look a bit strange, but here is what's going on there: an anonymous function is being created inside of what is called a function expression.  The function expression is the pair of paren's that wraps function () { ... }, and the invocation of the anonymous function simply lies in the (); but immediately after the function expression.  So, if you were to place an alert immediately before the Cell class comment, you would see the alert as soon as the page loaded, in fact, you would see the alert even prior to the full initialization of the DOM.  

Now, to the implementation of (2).  The Grid class below contains two properties and three methods.  The two properties serve as little HTML templates with substitution placeholders.  Those placeholder values are substituted within the Init method.  The Randomize method takes advantage of the fact that the "red/green/blue (rgb)" syntax is being used to define the background color of each rectangle.  Hence, random numbers between 0 and 255 can be generated fairly easily and those random numbers can be substituted within the jQuery css('backround-color' ...); function. 

 

   /**
* Grid Class
*/
Challenge.Grid = function () {

// property definitions and default initializations
//
this.RowTemplate = '<div class="row" id="row{0}"></div>';
this.ColumnTemplate = '<div class="col">row {0} column {1}</div>';
};

Challenge.Grid.prototype.Init = function(rows, columns) {

$('.full-inner').children().remove();

for (var i = 0; i < rows; i++) {

var rowIdSelector = "#row" + i;
var rowToAdd = this.RowTemplate.replace('{0}', i);

$('.full-inner').append(rowToAdd);

for (var j = 0; j < columns; j++) {

var columnToAdd = this.ColumnTemplate.replace('{0}', i);
columnToAdd = columnToAdd.replace('{1}', j);

$(rowIdSelector).append(columnToAdd);
}
}

$('.full-inner').css('resize', 'both');
};

Challenge.Grid.prototype.Randomize = function() {

$('.col').each(function () {

var r = getRandomInt(0, 255);
var g = getRandomInt(0, 255);
var b = getRandomInt(0, 255);
$(this).css('background-color', 'rgb(' + r + ', ' + g + ', ' + b + ')');
});
};

Challenge.Grid.prototype.Resize = function (rows, columns) {

this.Init(rows, columns);
};

 

Finally, the implementation of (3) which uses the not quite ready CSS3 "resize" property, supported in Google Chrome, Safari, and Firefox, but not any version of IE and not any mobile version of any browser.  So basically, it cannot be relied upon for a production release for GA (General Availability).  That said, this challenge didn't say anything about which browsers to support (lol).  Maybe full support was implied, my bad.  Anyway, let's see what the new CSS3 resize can do for us. For part three, we need to inherit from the Grid class.  Our new ResizableGrid class needs to both listen for changes to the size of the div which is rendering the grid on the page, while invoking the parent Grid class's Resize method when needed.  

 


    /**
     * Resizable Grid Class
     */
    Challenge.ResizableGrid = function () {
        
        // property definitions and default initializations
        //
        Challenge.Grid.call(this);
    
    };
    
    //Inherit Grid
    //
    Challenge.ResizableGrid.prototype = new Challenge.Grid();
    
    //Correct the constructor pointer
    //
    Challenge.ResizableGrid.prototype.constructor = Challenge.ResizableGrid;
    
    function getRandomInt(min, max) {
        return Math.floor(Math.random() * (max - min + 1) + min);
    }

    /*
    * Executes a setInterval for the life of the page to adust the resizable grid during a resize operation
    */
    Challenge.ResizableGrid.prototype.ResizeListener = function (numberOfRows, numberOfColumns) {

        var lastGridWidth = null;
        var lastGridHeight = null;
        var self = this;

        window.setInterval(function () {

            if (lastGridWidth == null) {

                lastGridWidth = $('.full-inner').outerWidth(true);
                lastGridHeight = $('.full-inner').outerHeight(true);
            } else {

                var currentGridWidth = $('.full-inner').outerWidth(true);
                var currentGridHeight = $('.full-inner').outerHeight(true);

                if (lastGridWidth != currentGridWidth) {

                    $('.full-inner').css('border', 'solid 1px gray');
		    var actualRowLength = 0;
		    var firstRow = $('.row').first();
		    var columnsInFirstRow = $(firstRow).find('.col');
		    var aCellWidth = $(columnsInFirstRow).first().outerWidth(true);
		    
		    $(columnsInFirstRow).each(function () {
			actualRowLength += $(this).outerWidth(true);
		    });
		    
                    if (currentGridWidth > lastGridWidth) {

			if ((currentGridWidth > actualRowLength) && ((currentGridWidth - actualRowLength) > aCellWidth)) {
			    
			    numberOfColumns += 1;
			    self.Resize(numberOfRows, numberOfColumns);
			    lastGridWidth = currentGridWidth;
			}
			
                    } else if (lastGridWidth > currentGridWidth) {

                        if (actualRowLength >= currentGridWidth) {

                            numberOfColumns -= 1;
                            self.Resize(numberOfRows, numberOfColumns);
                            lastGridWidth = currentGridWidth;
                        }
                    }
                }
		
		// Note: same adjustments for width can be made for height here.  This is just a first pass at the logic ...
		//
                if (lastGridHeight != currentGridHeight) {

                    $('.full-inner').css('border', 'solid 1px gray');
                    var colOuterHeight = $('.col').first().outerHeight(true);

                    if ((currentGridHeight > lastGridHeight) && ((currentGridHeight - lastGridHeight) > colOuterHeight)) {

                        numberOfRows += 1;
                        self.Resize(numberOfRows, numberOfColumns);
                        lastGridHeight = currentGridHeight;

                    } else if ((lastGridHeight > currentGridHeight) && ((lastGridHeight - currentGridHeight) >= colOuterHeight)) {

                        numberOfRows -= 1;
                        self.Resize(numberOfRows, numberOfColumns);
                        lastGridHeight = currentGridHeight;
                    }
                }
            }
        }, 500);
    };

 

More is going on here than just inheritance and size adjustments though. Notice how the constructor of the ResizableGrid class is replaced with itself. This is because the prototype of ResizableGrid was assigned to the Grid class to give us all of the nice features of the Grid. However, the assignment of the ResizableGrid's prototype to Grid also made the constructor of ResizableGrid the constructor of Grid. Not the intended effect of inheritance. Hence, the constructor replacement.

The sizing algorithm in the anonymous setInterval function determines if two main conditions are true for us.  One; is the width of the new grid large enough to contain another column of cells?  Two; is the height of the new grid large enough to contain another column of rows?  If so, the Resize method of Grid is called with a new number of columns and/or rows, respectively.  

In conclusion, and as a few notable mobile frameworks have already point out; JavaScript can be a powerful tool for not only data-driven logic, but UI and UX implementation.