Adding Event Handling to JavaScript Classes

Download code

The next item I need to address is object events. I have a mechanism for attaching event handlers to DOM objects, so when the user clicks a button, or changes the value of a text box, or does some other interaction with a DOM element, the View can handle that event. But then the View will need to fire its own event to notify a Controller object (yet to be created) that the user is attempting to accomplish some task.

For example, the user might click a Search button, or press the Enter key on a text box, both of which might fire the View’s “onSearch” event. A Controller listening for this event could then initiate the search. When the search is complete, the Controller could fire an event to indicate that the results are available. Maybe an “onSearchResults” event or something. Models will also need to fire events to notify other objects when data is loaded, changed, etc.

In fact, I figure there are very few objects that won’t need to fire events. Therefore, I decided to include events and event handling as a basic core functionality of all classes. This means revisiting the Class function and making some enhancements.

Here is the original Class function. If you are unfamiliar with it, you might want to review the original series about creating the Class object. It’s probably not necessary, but it might help you understand the changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
LivingMachines.Class = function(init) {
 
	// Create constructor function that will check if we are inheriting, 
	// then call real constructor
	var cstr = function() {
		if (arguments[0] === inheriting) return;
		this.construct.apply(this, arguments);
	};
 
	// If we are inheriting, copy the prototype, otherwise assign a new prototype
	if (init.inherits) {
		cstr.prototype = new init.inherits(inheriting);
		cstr.base = init.inherits.prototype;
		delete init.inherits;	// Keeps it from being added to the prototype later
	};
 
	// Copy init properties to the prototype (adds/overrides base class methods)
	for (var p in init)
		cstr.prototype[p] = init[p];
 
	// Return the constructor function (this is the class)
	return cstr;
 
};

The basic premise here is that I only need to add the event methods to a base class. Any descendant classes will then automatically inherit those methods. As I see it, the methods will be createEvent (to add an event to the class), listenTo (to add an event handler to an event), unlistenTo (to remove an event handler from an event, and yes I know that “unlisten” not a real word, but it works), and fire (to execute all event handlers that are attached to an event).

Since I am going to allow multiple event handlers, I will need an array to hold the handlers (which are just functions that will be executed when the event is fired). I decided to create an object, called events, that will hold an array for each event of the class. This will be an instance object, and so will need to be added to the constructor (line 7, below).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
LivingMachines.Class = function(init) {
 
	// Create constructor function that will check if we are inheriting, 
	// then call real constructor
	var cstr = function() {
		if (arguments[0] === inheriting) return;
		this.events = { };    // To hold our arrays of event handlers
		this.construct.apply(this, arguments);
	};
 
	// If we are inheriting, copy the prototype, otherwise assign a new prototype
	if (init.inherits) {
		cstr.prototype = new init.inherits(inheriting);
		cstr.base = init.inherits.prototype;
		delete init.inherits;	// Keeps it from being added to the prototype later
	};
 
	// Copy init properties to the prototype (adds/overrides base class methods)
	for (var p in init)
		cstr.prototype[p] = init[p];
 
	// Return the constructor function (this is the class)
	return cstr;
 
};

Next, I need to add the event methods. Since all classes will have event capabilities, I simply need to add event methods to the class’s prototype if this is a base class, that is, if this is a class that is not inherited from any other class. This will ensure that all top-level classes have event capabilities, and those capabilities will pass on to all descendant classes through inheritance.

If a class inherits from another class, then the inherits property of the init argument will be set, and I’m already checking for it (line 12). So, I simply need to add an else to the if, and add the event methods there (lines 17-47, below).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
LivingMachines.Class = function(init) {
 
    // Create constructor function that will check if we are inheriting, 
    // then call real constructor
    var cstr = function() {
        if (arguments[0] === inheriting) return;
        this.events = { };    // To hold our arrays of event handlers
        this.construct.apply(this, arguments);
    };
 
	// If we are inheriting, copy the prototype, otherwise assign a new prototype
    if (init.inherits) {
        cstr.prototype = new init.inherits(inheriting);
        cstr.base = init.inherits.prototype;
        delete init.inherits;	// Keeps it from being added to the prototype later
    }
    else {
 
        // Since we are not inheriting, then we must add event methods,
        // otherwise they will be included via inheritance.
 
        cstr.prototype.createEvents = function(/* event */) {
            for(var i = 0, len = arguments.length; i < len; i++)
                this.events[arguments[i]] = [ ];
        };
 
        cstr.prototype.listenTo = function(obj, event, handler) {
            obj.events[event].push( { handler: handler, scope: this } );
        };
 
        cstr.prototype.unlistenTo = function(obj, event, handler) {
            var ev = obj.events[event];
            // Loop down, just in case handler was added multiple times (and will be removed multiple times)
            for (var i = ev.length - 1; i >= 0; i--) {
                if (ev[i].handler === handler)
                    ev.splice(i, 1);     // Deletes one element starting at index i
            }
        };
 
        cstr.prototype.fire = function(event, data) {
            var handlers = this.events[event];
            for (var i = 0, len = handlers.length; i < len; i++) {
                var h = handlers[i];
                h.handler.call(h.scope, this, data);
            }
        }
    }
 
    // Copy init properties to the prototype (adds/overrides base class methods)
    for (var p in init)
        cstr.prototype[p] = init[p];
 
    // Return the constructor function (this is the class)
    return cstr;
 
};

The first event method, createEvents (line 22) takes a variable number of arguments. Each argument is the name of an event to be added to the class. It simply loops through each argument (line 23) and adds the name of the event to the events object (line 24). Each event is initialized to an empty array, and it’s these arrays that will hold all the handlers for a particular event.

The createEvents method should be called from the construct method of a class. I had to do it this way since each event handlers array has to be associated with a class instance, and not the class itself. So, for example, executing createEvents('onSave', 'onCancel') will add onSave and onCancel as properties to the events object (the one added in line 7). These properties are initialized as empty arrays. Since these are instance properties, each instance of the class can have a different set of handlers.

The listenTo method (line 27) is used to add an event handler to the appropriate event handler array. It takes three arguments, obj (the object that has the event to listen to), event (the name of the event to listen to), and handler (the function that will handle the event). It then adds the handler to the given event handlers array of the object that has the event (line 28). Note that this is actually pushing the handler function and this as a scope object into the handlers array. I will talk about why I am doing this when I describe the fire method.

The unlistenTo method (line 31) is used to remove a specific event handler from the list of handlers. It takes three arguments, obj (the object that has the event we are no longer interested in), event (the name of the event that the handler is to be removed from), and handler (the previously added function that is to be removed). This method first grabs the handlers list for the given event (line 32). Next, it loops backward through the handlers list (line 34). I typically loop backwards when removing items from a list. The reason is that I don’t have to adjust the index after an item is removed. If I’m on index 3, then whether or not I remove the item at index 3, the next index is always 2. If I were to loop forward, and I removed the item at index 3, then the next item to check is still index 3 (since the item at index 4 has moved down to 3). I also would need to adjust the stop index since the size of the array changed. When you loop backward, the stop index is always 0. Anyway, enough of that digression… It loops backward through the handlers list and if the function passed in matches one in the list (line 35) then it is removed (line 36). You can delete an array element with the delete keyword, but that will leave a gap in the array indexes, whereas using splice reindexes the array. Hence my use of splice over delete.

The last event method is the fire method (line 40). It takes two arguments, event (the event to be fired) and data (an object containing any data about the event). This method loops through all of the event handlers for the given event (line 42) and executes each one (line 44). This is where we need to be concerned with scope. If I were to simply call the handler function, then the this variable inside that function would point to the current object. That is, the this variable inside the handler function would point to the object that is firing the event. But what I really want is for the this variable inside the handler function to point to the object that is handling the event. That’s why I pass in the this variable as the scope object to the listenTo method when the handler is set up (line 28). Then, in the fire method, I can call the handler using that scope object, ensuring that the this variable in the handler is set to the object handling the event (line 44).

Here is a simple example of how to use this event mechanism.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var VariableSwitch = LivingMachines.Class({
    construct: function() {
        this.switchId = 'A';
        this.level = 0;
        createEvents('onLevelChanged');
    },
 
    set: function(level) {
        this.level = level;
        this.fire('onLevelChanged', level);
    }
});
 
var LightBulb = LivingMachines.Class({
    construct: function(sender, level) {
        this.brightness = 0;
    },
 
    setBrightness: function(sender, level) {
        this.brightness = level;
        alert('Brightness was set by switch ' + sender.switchId);
    }
});
 
var variableSwitch = new VariableSwitch();
var lightBulb = new LightBulb();
 
lightBulb.listenTo(variableSwitch, 'onLevelChanged', lightBulb.setBrightness);
 
variableSwitch.set(5);

I’m creating two sample classes here. The first is VariableSwitch (line 1-12). It has an switchId property so we can tell which switch it is (line 3), a level property to indicate the current level of the switch (line 4) and an onLevelChanged event that will be fired when the switch level is changed (line 5). This class also has one method, set (line 8), that will change the level of the switch (line 9) and fire the onLevelChanged event (line 10).

The second class is LightBulb (line 14-23). It has a single property, brightness (line 16) to indicate the current brightness of the bulb. It also has one method, setBrightness (line 19) that will set the brightness property to a given level (line 20) and alert the ID of the switch that caused the brightness to change (line 21). This function is an event handler. Notice the two arguments. sender is the object that sent the event, which will be an instance of VariableSwitch in our case, and level is the data object of the event.

Next I create an instance of VariableSwitch and LightBulb (lines 25 and 26). Then I attach the event handler (line 28). The lightBulb object will listen to the onLevelChanged event of the variableSwitch object. The lightBulb.setBrightness method will be executed when the event is fired.

Finally, I set the level of the variableSwitch (line 30) object. This will cause the onLevelChanged event to be fired, and the setBrightness event handler method will be executed, alerting that “Brightness was set by switch A”.

And voilĂ , JavaScript classes now have event capabilities.

Leave a Comment

Adding DOM Event Handlers to the View

Download code

So, I have some functions that will add DOM elements to the page, allowing me to build a View. Now, to allow the user to interact with the View, I need to add event handlers to the DOM elements.

There are basically three ways of doing this, one for W3C-compliant browsers, one for Internet Explorer, and one using browser-independent JavaScript. Personally, I prefer to use the lowest-common denominator when writing JavaScript. If I can avoid if blocks that cycle through browser-specific code, I’ll be much happier.

Browsers that follow the W3C standards (such as Firefox) use the addEventListener function to attach handlers to DOM elements. IE uses the attachEvent function instead. There are some differences between these two, but both allow for multiple handlers to be attached to a single event through multiple calls to the browser-appropriate function. The IE version, however, has one serious drawback: the this variable in the handler function always refers to the window object, rather than the DOM element that the handler is attached to like the other attachment mechanisms do. So while I could easily use browser detection to determine which function to use to attach handlers, I would still be stuck with the limitations of IE when that version was used.

The browser-agnostic version is to simply attach a handler function to the appropriate JavaScript event property directly. For example,

1
2
3
4
var btn = document.getElementById('submitButton');
btn.onclick = function() {
    alert('I have been clicked!');
};

The major drawback to this method is that you can only attach one function to the event. This is a limitation easily gotten around, however, unlike IE’s inability to give us a useful this variable in the handler. The benefit to this method is that it works on all browsers (even IE will give us a correct this variable inside the handler if we use this mechanism).

Now another drawback to event handling in general is the fact that the this variable will always point to the DOM element that the handler is attached to. Yeah, I know I said it’s a drawback when it doesn’t point to the element, but while it is useful to know this information, it does make event handling a bit misleading when using object-oriented programming. Consider the following:

1
2
3
4
5
6
7
8
var OkButton = {
    okClicked: false,
    handleClick: function() {
        this.okClicked = true;
    }
};
 
document.getElementById('ok-button').onclick = OkButton.handleClick;

Here I’m declaring a new object called OkButton (line 1). It has a property called okClicked (line 2) which should be set to true when the OK button is clicked. There is a handleClick function (line 3) which will be executed when the OK button is clicked, and will set the okClicked property to true (line 4). Line 8 attaches the handleClick function to the OK button.

Look carefully at line 4. What does the this variable refer to? Intuitively you would expect that it refers to the OkButton object, right? After all, that is typically what we use this for. But in this case, because the handleClick function is being executed from an event, the this variable will refer to the DOM element that the handler is attached to, not to the object that the handler function belongs to. This, to me, is a bit misleading and confusing. To make matters worse, if you call OkButton.handleClick directly, then the this variable will point to the OkButton object (since it was not executed from an event). Ugh, I hate ambiguity.

I would like to change all that, so that I can write JavaScript that is more intuitive and less ambiguous. I want the this variable to always refer to the object, not the DOM element, though I don’t want to lose the reference to the DOM element since it will be useful to have. I also want to be able to attach multiple handlers to a single event.

I have two functions to make that happen. One to attach an event to a DOM element, and another to dispatch an event to one or more event handlers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
LivingMachines.View = LivingMachines.Class({
 
    /* ... Lots of code excluded here for brevity ... */
 
    addDomHandler: function(elem, event, fn)
    {
        var ev = '_'+event;       // Add underscore to event name (e.g. _onmousedown, _onkeypress, _onclick)
        if (!elem[ev])
            elem[ev] = [ ];
        elem[ev].push(scopedFn(this, fn));
        elem[event] = this.dispatchDomEvent;
    },
 
    dispatchDomEvent: function(ev)
    {
        // "ev" is the event object
        // "this" is the element that this event handler is attached to
        // "elem" is the element that initiated the event (will be a child of "this", or might be "this")
 
        ev = ev || window.event;                      // IE uses global window.event
        var elem = ev.target || ev.srcElement;        // IE uses ev.srcElement
        var handlers = this['_on'+ev.type];           // e.g. ev.type = mousedown, keyup, click, etc.
        var result = true;
        for (var i = 0, len = handlers.length; i < len; i++) {
            result = handlers[i](this, elem, ev) && result;
        }
        return result;
    }
 
});

Obviously, these functions are part of the View class (line 1).

The first function is the addDomHandler method (line 5). This is used to attach an event handler function to a DOM element event. It takes three arguments. elem is the DOM element that we want to attach the event to. event is the name of the event (onclick, onmousedown, etc). These event names must match to DOM element events, so they must be in all lowercase letters. And fn is the handler function to be executed. If you want to add more than one handler to an event, you will need to call addDomHandler once for each handler to be added.

When the addDomHandler function is executed, it first adds an underscore to the event name (line7). So “onclick” becomes “_onclick”, for example. It then checks to see if the underscored name is a property of the DOM element (line 8). If it isn’t it creates the property as an empty array (line 9). This is the property that the handlers will be added to. This is what allows multiple handlers on a single event.

Next, the event handler function is pushed onto the handlers array property that was just created (line 10). The scopedFn function is part of the JavaScript Extensions I created several months ago. Basically, it ensures that a function is always executed in the scope of a given object. In other words, it ensures that the this variable in a function always points to a certain object. In this case, that object is the View (the this argument, which points to the View, is passed in to scopedFn). This is how the this argument in the handler is always guaranteed to reference the View object, and never the DOM element. Don’t worry, we’ll still have access to that DOM element. You’ll see how in a moment.

The final step this function does is to attach an event handler named dispatchDomEvent to the event itself (line 11). Note that we are using the event name that was passed in (not the underscored version). This is the actual event of the DOM element, the one that gets fired when the event occurs.

So at this point, we have a DOM element that has a property (such as “_onclick”, “_onmousedown”, “_onkeypress”, etc.) that is an array of scope-corrected handler functions. The DOM element’s event property (such as “onclick”, “onmousedown”, “onkeypress”, etc.) will execute the dispatchDomEvent method.

Now let’s take a look at what happens when an event is fired, causing the dispatchDomEvent to be executed. Remember, it will be executed when an event is fired. First, it get the correct event object. In most browsers, an event object is passed directly into the function (line 14). In Internet Explorer, the window object contains the event information. I’m using the OR syntax to get the correct value for ev (line 20). When an OR is executed, if the first expression has a value (is not null, undefined, an empty string or zero) then that value is returned from the OR operation and the second expression is never evaluated. If, however, the first expression evaluates to a “nothing” value, then the second expression is evaluated and returned from the OR operation. This ensures that the ev variable always points to the event object, regardless where it comes from.

Next, it grabs the target or source element (line 21). This is the element that the user interacted with, not necessarily the element that the handler is attached to. Why is that? Because events bubble. For example, we can attach an event handler to a DIV element. If the user clicks on an element within that DIV element, the event will bubble up the DOM hierarchy until it finds an event handler. In this case, the one attached to the DIV. So the handler attached to the DIV will be executed, but the source of the event is the element that the user actually clicked on. Most browsers keep this source element in the target property of the event object. Internet Explorer keeps it in the srcElement property. We use the same OR technique as before to get the correct source element.

Next, the function gets a pointer to the event handlers array that was added to the DOM element in the addDomHandler function (line 22).

Events can return true or false. If they return false, it will prevent the default action from happening for a given element. For example, a form submit button, when clicked, will submit the form unless the JavaScript onclick event handler function returns false. This will allow us to submit a form via AJAX, for example. Because we have multiple event handlers attached to the event, we need to track the value they each return. If any one of them returns false, then the dispatchDomEvent function (which is the function actually attached to the event) should return false as well. The result variable allows tracking of the event handler results (line 23).

Finally, it loops through all the handler functions (line 24). Remember that these are all scope-corrected functions, so when they execute, the this variable in each will point to the View object. However, since the dispatchDomEvent method is executed from an event, the this variable in that function (the one we’re looking at right now) will point to the DOM element that the handler was attached to. This is important as we look at actual execution of the handler functions (line 25). handlers points to the array of event handlers, and i is the looping index used to access each in turn. Since each array element points to a function, we can execute it using parenthesis, just like we do any function: handlers[i]( /* arguments */ ). The this variable is passed in as the first argument to the handler function. Remember, it points to the DOM element that the dispatchDomEvent function is attached to. The next argument passed in is the element that initiated the event. That is, the element that the user actually interacted with. The last argument passed in is the ev (event) object. Note that the function is ANDed with the current result value. If any event handler function returns false, then result will be permanently set to false for the remainder of the loop.

The last thing the function does is return the value of result (line 27).

Looking at how the event handler functions are executed, this means that event handler functions should have the following format:

1
2
3
4
5
6
7
8
9
10
11
MyView = LivingMachines.Class({
 
    inherits: LivingMachines.View,
 
    handlerFn: function(sender, source, ev) {
        // "sender" is the DOM element that the handler was attached to (the element that sent the event)
        // "source" is the DOM element that the user interacted with (the source of the event)
        // "ev" is the event object
        // "this" points to an instance of MyView
    }
});

And there you have it. An event handlers mechanism that allows multiple event handlers to be attached to a single event, corrects the this variable to always point to the View object, and gives us access to the DOM element that the handler is attached to and the DOM element that initiated the event.

Comments (2)

Creating a Select Element that Plays Well with All Browsers

Download code

Creating DOM elements like text and password INPUTs, BUTTONs and TEXTAREAs is pretty simple. Creating a SELECT element, however, proved to be a little more tricky thanks to a quirk in the Opera browser.

Let’s look at a SELECT tag. It’s pretty simple, really. It has a SELECT tag with a number of OPTION tags inside it.

1
2
3
4
5
<select id="colors" name="rgb_color">
    <option value="r">Red</option>
    <option value="g" selected="true">Green</option>
    <option value="b">Blue</option>
</select>

Each OPTION tag has a value attribute. If this particular SELECT element was submitted from a FORM, it would send to the server the name ‘rgb_color’ with the value ‘g’, since that’s the option that is currently selected.

From this example, it would seem that I could create a SELECT element, then add the OPTION elements to it with the selected attribute set for the appropriate option. Using the View’s DOM rendering functions, that would look something like this (in View.js).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
select: function(id, selected, options, props)
{
    if (!props) props = { };
    props.id = id;
 
    var optionElements = [ ];
 
    if (options) {
 
        for (var value in options) {
 
            var optionProps = {
                selected: value == selected,
                value: value
            };
 
            optionElements.push( this.createElement('option', optionProps, [options[value]]) );
        }
    }
 
    return this.createElement('select', props, optionElements); 
}

The first argument is the id of the SELECT element. The selected argument is the value of the currently selected OPTION element. This will allow us to set an option as already selected. The options argument is an object that defines which OPTIONs are displayed in the SELECT drop down list. The last argument is any additional properties to be set on the SELECT element. So, to create the SELECT element in our example above, we would make the following call.

1
2
3
4
5
myView.select('colors', 'g', {
    r: 'Red',
    g: 'Green',
    b: 'Blue'
}, { name: 'rgb_color' } );

When the select method is executed with the arguments in this call, it will loop through each of the options (lines 10 to 18 in the select function source code, above) and create an optionProps object for that option (lines 12 to 15). The optionProps object contains the value (which is the property name in the options object, ‘r’, ‘g’ or ‘b’ in this example) and the selected property, which is set to true if the selected argument matches the value of the current option. Next, it uses the createElement method to create the OPTION element (line 17), passing in the option text as a child (so that it appears between the OPTION’s open and close tags). The third argument of createElement expects an array, which is why the argument is surrounded in square brackets. Finally, the new option is added to the optionElements array using the array’s push function.

Now that the OPTION elements are created, the only thing left to do is create the SELECT element, and add the OPTION elements to it. Using the createElement function, this is very simple (line 21).

And it works like a charm in all browsers except Opera. For some reason, Opera does not like the selected property for the OPTION element. I’ve tried different variations, but none seem to work.

The solution is to use the selectedIndex property of the SELECT element. This is supported by all browsers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
select: function(id, selected, options, props)
{
    if (!props) props = { };
    props.id = id;
 
    var optionElements = [ ];
    var selectedIndex = 0;
 
    if (options) {
 
        for (var value in options) {
 
            var optionProps = {
                //selected: value == selected,  // Not supported by Opera
                value: value
            };
 
            if (value == selected)
                selectedIndex = optionElements.length;
 
            optionElements.push( this.createElement('option', optionProps, [options[value]]) );
        }
    }
 
    var sel = this.createElement('select', props, optionElements);
    sel.selectedIndex = selectedIndex;
 
    return sel;
}

Line 7 adds a new variable to track the selected OPTION element, and is defaulted to zero (the first OPTION element). In line 14, I’ve removed the selected property, as it will not be used. Line 18 checks if the current option’s value matches the selected argument and, if it does, sets the selectedIndex appropriately (line 19). Line 25 creates the SELECT element, and line 26 sets the selectedIndex of the SELECT element. I have to set the selectedIndex after creating the element since the current version of createElement applies the props object before the child OPTION elements are added, and you can’t set selectedIndex when there are no OPTION elements. If this were not the case, if the props object was applied to the element after all OPTION elements were added, then I could just add selectedIndex to the props object. Hmm. Maybe a change to createElement is in order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
createElement: function(tag, props, elements)
{
    // Create the element
    var elem = document.createElement(tag);
 
    // MOVE THIS BLOCK DOWN TO LINE 24
    //window.copy(props, elem, function(data) {
    //    if (data.prop == 'cssFloat' && !elem.cssFloat)
    //        data.prop = 'styleFloat';
    //});
 
    // Add any inner element to the newly created DOM object        
    if (elements) {
 
        for (var i = 0, len = elements.length; i < len; i++) {
            if (isString(elements[i]))
                elem.appendChild(this.element(elements[i]));
            else if (elements[i])   // Make sure not null or undefined
                elem.appendChild(elements[i]);
        }
    }
 
    // Apply the properties to the element, correct property names where necessary (Globals.js)
    window.copy(props, elem, function(data) {
        if (data.prop == 'cssFloat' && !elem.cssFloat)
            data.prop = 'styleFloat';
    });
 
    // Lastly, return our new DOM element
    return elem;
}

That’s better. Now the props object will be applied after all child elements have been added. All unit tests still pass with this change. Will it break something else or cause problems in the future? Maybe. But for now, it simplifies the code for the select method, since I no longer need to track selectedIndex separately. Instead, I can update it in the props object directly. This also allows the selectedIndex property to be passed in to the props argument and have it be set correctly. That gives another option for setting the currently selected option other than the selected argument.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
select: function(id, selected, options, props)
{
    if (!props) props = { };
    props.id = id;
 
    var optionElements = [ ];
 
    if (options) {
 
        for (var value in options) {
 
            var optionProps = {
                value: value
            };
 
            if (value == selected)
                props.selectedIndex = optionElements.length;
 
            optionElements.push( this.createElement('option', optionProps, [options[value]]) );
        }
    }
 
    return this.createElement('select', props, optionElements);
}

Note that the selectedIndex variable has been removed. If the option value and the selected argument values match, then line 17 sets the selectedIndex property of the props object. Thanks to JavaScript’s mutable objects, if this property doesn’t already exist, it will be created. Line 23 is the call to the createElement method to create the SELECT element and set the properties.

If you compare the original version (the one that didn’t work with Opera) to this version, one line of code was deleted and two were added, for a net result of one additional line of code. Not too bad when trying to correct browser incompatibilities.

So, there’s the final version for now. Of course, this does not support multiple option selects. That’s for another time, I think.

Leave a Comment

Copying DOM Element Properties and Handling Browser Differences

Download code

I’ve made another slight, but significant, change to the way in which DOM elements are created by the View. Going back to my original approach of building DOM elements from HTML strings, it made sense to refer directly to HTML and CSS attributes. But now that I’m creating DOM elements directly through document.createElement, there is no need to use HTML attributes, and I can just use the DOM element properties.

The first change, then, was to rename attribs (attributes) in all DOM element rendering functions to props (properties).

Next, I updated the view’s createElement function (in View.js, not document.createElement). This is the original version.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
createElement: function(tag, attribs, elements)
{
    // Create the element
    var elem = document.createElement(tag);
 
    // Add the attributes to the element. HTML attribute names do not always match
    // the name that the DOM uses, so we check for them here.  This may not be all
    // of them yet!
    for (var a in attribs) {
        attr = a.toLowerCase();
        if (attr == 'class')
            elem.className = attribs[a];
        else if (attr == 'for')
            elem.htmlFor = attribs[a];
        else
            elem.setAttribute(a, attribs[a]);
    }
 
    // Add any inner element to the newly created DOM object        
    if (elements) {
 
        for (var i = 0, len = elements.length; i < len; i++) {
            if (isString(elements[i]))
                elem.appendChild(this.element(elements[i]));
            else if (elements[i])   // Make sure not null or undefined
                elem.appendChild(elements[i]);
        }
    }
 
    // Lastly, return our new DOM element
    return elem;
}

In the new version (below) note that the second argument has been changed from attribs to props. It now expects DOM element property names, not HTML attribute names. This means I need to use names like className instead of class, and htmlFor instead of for. Since this function no longer has to do attribute name to property name conversions (since I’m now passing it property names and not attribute names), lines 6 through 10 (lines 6 through 17 in the original version) have been updated to use the copy function (in Globals.js, part of the JavaScript Extensions). I’ll explain that third argument to the copy function, the anonymous function with the data argument, in a moment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
createElement: function(tag, props, elements)
{
    // Create the element
    var elem = document.createElement(tag);
 
    // Apply the properties to the element, correct property names where necessary (Globals.js)
    window.copy(props, elem, function(data) {
        if (data.prop == 'cssFloat' && !elem.cssFloat)
            data.prop = 'styleFloat';
    });
 
    // Add any inner element to the newly created DOM object        
    if (elements) {
 
        for (var i = 0, len = elements.length; i < len; i++) {
            if (isString(elements[i]))
                elem.appendChild(this.element(elements[i]));
            else if (elements[i])   // Make sure not null or undefined
                elem.appendChild(elements[i]);
        }
    }
 
    // Lastly, return our new DOM element
    return elem;
}

There are some special cases to handle when copying properties, so I needed to alter the copy function, too. These special cases are due to browser differences. Like the fact that all browsers use the style.cssFloat property, but IE uses style.styleFloat. If I’m going to use the copy function, a change is required to handle these special scenarios. But since the copy function is an extension, I want to keep the change generic.

The copy function originally took two arguments. The first was the source object, and the second was the destination object. Now, there is a third argument. This argument is a function that will be called for every property in the source object just before it is added to the destination object. This allows the property name, value or data type to be changed before it is copied to the destination object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
window.copy = function(src, dest, fn)
{
    if (!dest) 
        dest = {};
    for (var prop in src) {
        var value = src[prop];
        var type = window.typeOf(value);
        if (fn) {
            var data = { prop: prop, value: value, type: type };
            fn(data);
            prop = data.prop;
            value = data.value;
            type = data.type;
        }
        if (type == 'object') {
            if (!dest[prop])
                dest[prop] = {};
            window.copy(value, dest[prop]);
        }
        else if (type == 'date') {
            dest[prop] = new Date(value.getTime());
        }
        else if (type == 'array') {
            dest[prop] = value.slice();
        }
        else {
            dest[prop] = value;
        }
    };
 
    return dest;
};

Lines 8 through 14 check if a callback function (fn) was passed in and, if so, creates an object that has three properties: the property name, value and data type. This data object is then passed to the callback function. Why use an object, rather than passing the values in as three separate arguments? Because objects are passed by reference. This means that the callback function data object points to the same data object as the copy function, so changing a value in the callback function also changes that value in the copy function (since both are pointing to the same data object). If I was passing in three separate arguments, they would be local only to the callback function, so changing their values would not change them in the copy function.

If you look back at the modified code for the View’s createElement function above, you’ll see that the callback function checks to see if the property is cssFloat and, if it is, checks to see if that property exists in the DOM element. If it doesn’t, it means we are running in IE and must use the styleFloat property name. In that case, it changes the property name.

So adding the callback function to copy allows me to change property names to suit different browsers, but still keep the copy function generic.

The other advantage to using the copy function that that it handles nested objects. So, specifying styles can be done with a props object like the following passed to the View’s createElement function. Note that these are DOM element property names, not CSS attribute names.

1
2
3
4
5
6
7
8
{
    id: 'someId',
    style: {
        display: 'block',
        backgroundColor: '#cccccc',
        cssFloat: 'right'
    }
}

So, not any major changes here, but they are significant from the perspective of using the View. It also simplifies the View’s createElement function a little more, but still keeps it flexible.

Leave a Comment

Refining the Creation of DOM Elements

Download code

If you recall from my first stab at creating the MVC View, I had a method of the View class called tag that would take some arguments and create a DOM element from them. This tag method would be called from higher-level methods designed to make creating a DOM element for the View easier. For example, the form method below (also a member of the View class) is used to create a FORM element. The tag method is called in line 9, and it does the actual work of creating the FORM element:

1
2
3
4
5
6
7
8
9
10
form: function(id, url, method, attribs /*, elements*/)
{
    if (!attribs) attribs = { };
 
    attribs.id = id;
    attribs.action = url;
    attribs.method = method;
 
    return this.tag('<form %a></form>', attribs, this.extractArgs(arguments, 4));
}

Now, let me explain a few things. First, I had envisioned calling tag with more options, like this:

1
return this.tag('<form id="%i" action="%v" %a>%e</form>', id, url, attribs, elements);

The %i would be replace with the id, %v would be the value (the url argument in this example), %a would be additional attributes, and %e would be the other elements contained by this element. All elements would include these arguments, so an INPUT element would be created something like this:

1
return this.tag('<input id="%i" value="%v" %a />', id, value, attribs, null);

What I realized, however, was that %i and %v were superfluous, and could be eliminated. Since these were just attributes, they could simply be added as part of %a. And this is exactly what I did in the first version of the tag function that I posted.

The %e I realized would require me to work strictly with HTML strings. In other words, I could replace the %e with an HTML string, but not with an actual DOM element (I can’t insert an object into the format string). The better plan, I figured, was to convert any HTML strings into DOM elements, then add the DOM elements to the containing element created by the tag method. That gave me more flexibility with what I passed as elements to the tag method. I could pass in an HTML string and convert it, or just pass in a DOM element. It also eliminated the need for the %e. Hence it was also dropped in the previous version of the tag method.

That left a much simpler structure, though it still had its roots in working with HTML strings. The attributes were expected to be passed into the tag method as an object of name/value pairs. That object would then be converted into an attributes string, and the %a in the format argument would be replaced with it. So essentially, a call to the tag method, like in line 9 below…

1
2
3
4
5
6
7
8
9
10
text: function(id, value, attribs) 
{
    if (!attribs) attribs = { };
 
    attribs.type = 'text';
    attribs.id = id;
    attribs.value = value;
 
    return this.tag('<input %a />', attribs);
},

…would result in the following code being executed inside the tag method…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Convert the attribs object to an HTML string.
var attribsHtml = '';
var singleAttrib = '.compact.checked.declare.readonly.disabled.selected.defer.ismap.nohref.noshade.nowrap.multiple.noresize.';
for (var a in attribs) {
    if (singleAttrib.indexOf('.' + a + '.') > -1) {
        if ((attribs[a] == 1) || (attribs[a] == true) || (attribs[a] == 'true') || (attribs[a] == a))
            attribsHtml += ' ' + a;
    }
    else {
        if (attribs[a]) // Make sure its not null.
            attribsHtml += ' ' + a + '="' + attribs[a] + '"';
    }
}
 
// Insert the attributes into the tag format
format = format.replace('%a', attribsHtml);

…which would create an attributes string (lines 1-13) that would then be inserted into the format argument at the %a position (line 16). In this particular example, this would result in an HTML string like the following:

1
format = '<input type="text" id="username" value="jdoe" />'

I could then take this HTML string, this complete tag, and convert it into a DOM element with a call to the element method. In fact, this is exactly what the tag method did next.

1
2
// Convert the HTML string to a DOM object
var elem = this.element(format);

So that’s where it stood as per my last post, and up until recently, it worked nicely.

However, I began working on a method to create a TABLE element. I got as far as attempting to create the TR and TD elements. Turns out, these cannot be created anywhere except inside a TABLE element, which meant creating them using the tag method was impossible. You see, the element method (which the tag method uses to actually create the DOM element) creates a DIV, sets the innerHTML of the DIV to the HTML string that was created, then extracts the DOM element that the browser created from the HTML. But TR and TD elements can’t sit inside a DIV element, so the browser (at least Firefox) will not create them using the element method.

I really dislike making special cases and wanted to avoid that if at all possible. In other words, I didn’t want to check if the tag passed in was a TR or a TD and then work some different magic to create them as DOM elements. I wanted to use the same mechanism, regardless of the element being created.

I could create the TABLE element as a single string, with all the TR and TD elements inside it, but that would mean separate code to create the attribute strings, or pulling the attribute string generating code into a separate method. It would also mean I could wind up concatenating a huge HTML string, depending on how big the table was. Also, in order to keep the same mechanism where the programmer could pass in HTML strings or DOM elements, I would potentially have to convert a DOM element (one that might appear in a table cell, for example) into a string so that I could build it into the table. The more I thought about it, the more convoluted the whole processes seemed.

It was at this point that I realized five things. First, all the tags had the same format: '<tag %a></tag>'. Only the tag name was different. Second, any element can be created using the document.createElement function, even if that element can only sit inside specific other elements (like TR and TD can only sit inside a TABLE element). Third, all you need is the tag name to use document.createElement. Fourth, attributes can be added directly to a DOM element; they don’t need to be name/value pair strings. Fifth, I really didn’t like the name “tag” for the method, since it does not describe what the method really does.

So, I set out to refactor the tag method. First, I changed the name of the method to createElement, which more accurately describes what it does. Next, I
renamed the first argument from format to tag. Now, it expects only a tag name, not a tag format. And %a is no longer needed.

1
2
3
4
// Original
tag: function(format, attribs, elements)
// New
createElement: function(tag, attribs, elements)

So, the part of the form method that calls the tag method changes, too.

1
2
3
4
5
6
7
8
9
10
11
12
13
form: function(id, url, method, attribs /*, elements*/)
{
    if (!attribs) attribs = { };
 
    attribs.id = id;
    attribs.action = url;
    attribs.method = method;
 
    // Original
    return this.tag('<form %a></form>', attribs, this.extractArgs(arguments, 4));
    // New
    return this.createElement('form', attribs, this.extractArgs(arguments, 4));
}

I still have concerns about the method name. I can see where some people would confuse the View’s createElement method for the document’s createElement function. They are not the same, but the name accurately describes what they do. So for now, I will leave it with the new name.

Incidentally, if you are not familiar with the extractArgs method, you can learn about it in my first post on creating the MVC view.

Finally, the tag method, now renamed to createElement, got some internal changes as well. It now uses the document.createElement function to create elements, and it adds attributes directly to the newly created DOM element. The following is the updated function in its entirety.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
createElement: function(tag, attribs, elements)
{
    // Create the element
    var elem = document.createElement(tag);
 
    // Add the attributes to the element. HTML attribute names do not always match
    // the name that the DOM uses, so we check for them here.  This may not be all
    // of them yet!
    for (var a in attribs) {
        attr = a.toLowerCase();
        if (attr == 'class')
            elem.className = attribs[a];
        else if (attr == 'for')
            elem.htmlFor = attribs[a];
        else
            elem.setAttribute(a, attribs[a]);
    }
 
    // Add any inner element to the newly created DOM object        
    if (elements) {
 
        for (var i = 0, len = elements.length; i < len; i++) {
            if (isString(elements[i]))
                elem.appendChild(this.element(elements[i]));
            else if (elements[i])   // Make sure not null or undefined
                elem.appendChild(elements[i]);
        }
    }
 
    // Lastly, return our new DOM element
    return elem;
}

First, it creates the DOM element (line 4). Then it adds the attributes to the element (lines 6-17). It is expected that these attributes are passed in with names that map to HTML attributes. The DOM property names may be different. For example, the HTML class attribute maps to the DOM element className property. I haven’t gone through all of the attributes yet, so this is probably not a complete list. As a bonus, now that I’m not creating a string of attributes, the code is much simpler.

The rest of the method is the same as the original, and uses the element method where necessary to convert HTML strings into DOM elements. I think this will be fine since it is unlikely that we will be passing HTML strings in for inner elements that cannot be created inside a DIV element. The table rendering method takes care of creating those elements that can only appear inside a TABLE element, and anything that we might pass in to be created inside a TD element should be able to be created inside a DIV, just the way the element method does it.

In my next post, I’ll take a closer look at that table method that started this whole refactoring process. For those who are interested in taking a look at it now, it’s available if you download the source code.

One final note. The test scripts for this are by no means complete and are, quite frankly, rather sloppy at the moment. I’ll get them cleaned up soon.

Leave a Comment