HtmlControlWapper Javascript

by Jason Haley 15. April 2008 15:44

Last week, I started a simple Html prototype for one of the guys in the office.  It was to be one of those simple html pages with a little javascript that I've done time and time again (ie. hack and boring). 

So I started with an existing web page and began adding the new parts ... then decided I'd done this too many times before - just to end up with a bunch of duplicate javascript ... which I didn't want to do this time.  In the past 10 years or so, I've done quite a few Html prototypes and last week I finally decided to take the time and put together some utility javascript functions to make writing these little prototypes easier. 

Downloads

Sample page: http://jasonhaley.com/files/HtmlControlWrapperExample.htm

Javascript source file: http://jasonhaley.com/files/HtmlControlWrapper.js

The Goal
To create some functions to create an easy way to bind javascript to html controls on a page with as little effort as possible and end up with clean javascript that can easily be edited.

The Approach

Initially I started writing the javascript code for a simple scenario: Person and Address (I added Books later on for testing a multi select list).  The first objective was to bind the controls for collecting an address to a javascript object in order to populate default values and to have the javascript object updated whenever image a user changed the control values.  So I had an object (or function) defined with the each of the properties defined, an Initialize function that would set all the properties to default values and store references to the controls being wrapped, I also had event handlers for the different controls.  After I got the Person and Address working fine, I started factoring out the duplicate code between the two objects to see if I could end up with a single utility object that could handle both - and in the process cut the size of code by more than half.

The first things I factored out were the event handlers ... but to do that I needed to create a 'mapping' object that contained the control-to-property relationships ... something that would tell me that txtFirstName goes with the property FirstName.  Once I create the map objects and refactored the event handlers into a single OnChanged handler, I noticed the storing of the controls in variables (such as this.txtFirstName) and the properties could also be created from this new map object (since it had the names of both the controls and the properties) so I got rid of the property declarations too.  The example (link is mentioned above) is the end product of this refactoring.

The Example: HtmlControlWrapper

The example is a web page that collects the typical name, address fields.   In order to create a javascript object to wrap and handle all the controls shown in the image to the bellow, 3 requires map objects (could be just one) and a call to the HtmlControlWrapper object.  In the sample, I created a the maps before the call to the imageconstructor, but they could just as easily be passed in.

Here are the maps

// Create a map for each of the 'objects'
var personMap = { 
    txtFirstName : "FirstName", 
    txtLastName : "LastName",
    optMale : "Gender",
    optFemale : "Gender"
    };
var addressMap = {
    txtAddressLine : "AddressLine",
    txtCity : "City",
    ddlState : "State",
    txtZip : "Zip",
    chkHomeAddress : "IsHomeAddress"
    };
 
var bookMap = { 
    lstBooks : "SelectedBooks" 
    };

As you can see, the maps are just the name of the control and the property name to be linked to that control.  The map object is used for creating properties on the javascript object and for wiring the event handlers to the property property.

NOTE: One odd thing in the current javascript is the binding of radio buttons.  The current logic requires radio buttons to have a value attribute (which is what the property value will be set to, and vice versa) and when shown in the Show() method, you'll notice both the optMale and optFemale will have the same value.  I suppose I should set the map to take an array of controls for this type of situation, but currently what I have works good enough for me.

In order to make the javascript object act like the Person object contains an Address object, I allow child objects to be passed in constructor.  You can pass as many children in as you want - the javascript just takes the Name and adds it as a property to the this instance, setting the value to what you pass in as the Child.  For example the following call creates a wrapper with the personMap and adds a child named Address which is another wrapper created with the addressMap:

var p = new HtmlControlWrapper(personMap, {Name: "Address", Child: new HtmlControlWrapper(addressMap) });

The javascript object lets you write code like the following to set the properties of the person and the address:

// Set default values
p.FirstName="Jason";
p.LastName="Haley";
p.Gender = "Male";
p.Address.AddressLine = "1st Ave";
p.Address.City = "Seattle";
p.Address.State = "WA";
p.Address.Zip = "98101";
p.Address.IsHomeAddress = true;
 
// Call populate to have the controls set to the object's values
p.Populate();

Even though the javascript object is a pretty thin wrapper over the controls, it gives you an interface that seems comfortable and easy to use - all driven from the map objects.

With the current implementation, after you set the default values - you still need to call the Populate() method to cause the object values to be reflected in the UI controls.

The default behavior of the javascript is to wire up the OnChanged function to the following events depending on the type of control:

Control Type Event
INPUT type=text blur
INPUT all other click
SELECT change
All other click

NOTE: currently only handles the controls shown in the example INPUT (text, radio, checkbox) SELECT (multiple and non mulitple)

The OnChanged event handler just binds the control's selected value to the property of the javascript object, so if you need to do anything outside of getting the selected value - you need to add your own event handler.  Your event handler needs to follow the following naming standard: On<PropertyName>Changed.

Here is an example of wiring a custom event handler to a function declared elsewhere in the code:

p.Address.OnZipChanged = SeattleZip;

Here is an example of a simple event handler:

// Simple example of business logic
function SeattleZip(e)
{
    if (p.Address.Zip == "98101")
    {
        p.Address.City = "Seattle";
        p.Address.State = "WA";
 
        // Currently have to still call Populate() to update the UI, could use setter/getter to eliminate 
        p.Populate();
    }
    else if (p.Address.Zip == "01970")
    {
        p.Address.City = "Salem";
        p.Address.State = "MA";
 
        p.Populate();
    }
 
    // Currently no bubbling, so call custom event handler I know is effected
    p.Address.OnStateChanged();
}

You can also set a custom event handler to a function like this:

p.Address.OnStateChanged = function(e) 
    { 
        if (p.Address.State == "WA") 
            showElement("divWashington"); 
        else 
            hideElement("divWashington"); 
    };

The javascript object only has a few functions - mainly what is needed to write some simple demo or prototype logic (hide, show, enable, disable, etc.).  Here is what I currently have in the included javascript file:

HtmlControlWrapper

Constructor (map, children) Initializes the Map property, creates the children passed in and populates the properties of the object from the map.
OnChanged(e) Default event handler, sets property that belongs to the control that called the event handler (determined from the Map object)
Populate() Causes the controls to be updated with the property values.  This is only one way - takes the values from the properties and sets those values in the controls.
Show(map) Creates and shows a message containing the current values of the properties on the javascript object, using the map passed in or the one on the object if a map object is not passed in.
Map property Reference to the map passed in the constructor.

Utility methods

getChangeEvent(name) Determines the name of the event to use for wiring the properties to controls.
addEvent(obj, evt, handler) Wires the event handler to the Html control.  Compatible with IE and FireFox.
getSelectedValue(element) Gets the selected value(s) for a given control.  Uses properties of the Html control to determine what values to return.
setValue(element, value) Partner function to getSelectedValue().  Sets the value of a control to the value(s) passed in.  Uses properties of the Html control to determine what properties to set.
$(id) Shortcut method for getElementById
getTarget(id) Gets the target from an event object.  Either returns the target or the srcElement property.
hideElement(id) Sets the display = "none" for the element of the given id.
showElement(id) Sets the display = "" for the element of the given id.
toggleDisplay(id) Either hides or shows the element of the given id, depending on the current state of the display.
disableElement(id) Sets the disabled property to true for the element of the given id.
endableElement(id) Sets the disabled property to false for the element of the given id.

After using the code for the linked sample and for the demo at work there are a few things I would like to address:

Radio buttons - current functionality works, but I would rather have the map be a little smarter and keep a control array for a single property.

Event Handlers - should bubble up to cause other event handlers to fire (currently have to call the function manually).  This can be seen in the example of the OnZipChanged method: SeattleZip().  At the end of the function, you'll see I have a call to p.Address.OnStateChanged().

Data binding - currently it is only one way - which means you have to set the property, then call Populate() in order to get the UI to update.  Would be nice to just set the property and have the UI update itself.

Summary

In the end, this small set of javascript functions saved me a lot of time and duplicate code in writing a simple javascript/Html prototype.  The result is much cleaner javascript that will be easier to change when those little requests to make 'little' changes to it happen.

This little exercise gave me a chance to look back through David Flanagan's JavaScript: The Definitive Guide quite a bit to re-familiarize myself with some of pieces of javascript that I don't frequently use.

Comments (0) | Post RSSRSS comment feed |

Categories:
Tags:

Comments are closed