jQuery & International Address Data – Collecting and Storing

In one of my websites I am required to take address information from the US, Canada, Germany and the list is growing. I needed a way to collect and store this information in a way that reports could be generated from this data. The reports needed to be filtered by country, state/province, etc. I searched Stack Overflow for some answers and came up with numerous results. I also ran across this post on displaying international address fields on a web page.

There is much debate on how this information should be collected and stored. The easiest and most accurate way would be to have a multi-line text box that allows the user to enter his/her address. This would allow the user to enter the correct formatting, required fields, etc. I like that approach, but unfortunately it doesn’t work very well when creating reports and querying the data. A parser could be used to parse this information and store it in the database, but that isn’t always accurate and wastes CPU cycles.

Another approach is to ask for any number address lines, city/town, state/province/region, and zip/postal code. None of these fields should be required, because the user may not use all of the fields in the format for his/her country’s address.

Then I took a look at how Google handles this. I figured they were international and probably dealt with this sort of thing before. I logged in and went to edit my address. Google allows me to select my country and then format the fields I need to enter for that country. I thought this would be fairly easy to do with some jQuery magic, so I did and thought I’d share it and get your comments and suggestions.

(function($) {
    var _fields = {
        address1: { id: "address1", name: "address1" },
        address2: { id: "address2", name: "address2" },
        address3: { id: "address3", name: "address3" },
        locality: { id: "locality", name: "locality" },
        region: { id: "region", name: "region" },
        postCode: { id: "postCode", name: "postCode" }
    };
    var _labels = {
        address1: "Address:",
        address2: "",
        address3: "",
        area: "Area:",
        city: "City:",
        county: "County:",
        department: "Department:",
        district: "District:",
        emirate: "Emirate:",
        island: "Island:",
        locality: "City/Town:",
        metroProvince: "Metro/Province:",
        region: "State/Province/Region:",
        postalCode: "Postal Code:",
        postCode: "Zip/Postal Code:",
        prefecture: "Prefecture:",
        province: "Province:",
        state: "State:",
        zip: "Zip:"
    };
    var _layouts = {};
    var _templates = {
        text: function(data) {
            return "<label for=\"" + data.id + "\">" + data.label + "</label><input id=\"" + data.id + "\" name=\"" + data.name + "\" type=\"text\"/>";
        },
        select: function(data) {
            var html = "<label for=\"" + data.id + "\">" + data.label + "</label><select id=\"" + data.id + "\" name=\"" + data.name + "\" disabled=\"disabled\"><option selected=\"selected\">Loading...</option></select>";
            data.source(function(regions) {
                var html2 = "";
                $.each(regions, function(i, item) {
                    html2 += "<option value=\"" + item.value + "\">" + item.text + "</option>";
                });
                $("#" + data.id).removeAttr("disabled").html(html2);
            });
            return html;
        }
    };
    var _defaultLayout = [
        { fieldName: "address1", templateName: "text", labelName: "address1" },
        { fieldName: "address2", templateName: "text", labelName: "address2" },
        { fieldName: "address3", templateName: "text", labelName: "address3" },
        { fieldName: "locality", templateName: "text", labelName: "locality" },
        { fieldName: "region", templateName: "text", labelName: "region" },
        { fieldName: "postCode", templateName: "text", labelName: "postCode" }
    ];
    $.fn.globalAddress = function(countrySelect, settings) {
        settings = $.extend(true, {}, {
            labels: _labels,
            fields: _fields,
            templates: _templates
        }, settings);
        var addressContainer = $(this);
        $(countrySelect).change(function(e) {
            var country = $(this).val();
            var layout = _layouts[country] ? _layouts[country] : _defaultLayout;
            var html = "";
            $.each(layout, function(i, l) {
                var data = $.extend(true, { label: _labels[l.labelName] }, settings.fields[l.fieldName], l.data);
                html += settings.templates[l.templateName](data);
            });
            $(addressContainer).html($(html));
        }).trigger("change");
        return this;
    };
    $.globalAddressExtend = function(key, layout) {
        _layouts[key] = layout;
    };
})(jQuery)

First I made some local variables to hold the settings for the global address display. The fields object stores information about the different fields of the object. This data is used in the template functions for the id, name, class and other attributes for the input elements on the form. Next I define the labels that will be used as the text for the label element. Then I define some templates to be used for different form elements like <input type=”text”/> and <select></select>. The data argument of the template function contains label text, field attributes and data source (in the case of a <select>). These variables can be overridden by specifying them in the plugin function. More on this later.

The _layouts variable contains the array of layout definitions that define what fields and in what order the fields should be included on the page. These are defined through a call to $.globalAddressExtend which will add the layout based on the specified key.

Next the plugin function. This function takes a <select> element that has the list of countries and a settings object that will allow the default settings (_fields, _labels and _templates) to be overridden. After extending the settings object, I make a variable to the jQuery element so I can append the formatted display. Then an event handler is attached to the country <select> element. When the country changes, I get the layout array from the _layouts variable. If no layout exists, I grab the default layout. Next, each member of the layout array is looped through and the html is appended to the jQuery element that is a container where the HTML is displayed.

The template function is given a “data” argument that contains all the information needed to build the HTML. It contains the id, name, label text, etc. and in the case of a <select> element, it contains a source function that gets the data for options of the <select>. The source function could get the data from anywhere (I’m using geoname.org).

So, if I wanted a country list with US and Canada, I would add the layouts like the following

(function($) {
    $.globalAddressExtend("US", [
        { fieldName: "address1", templateName: "text", labelName: "address1" },
        { fieldName: "address2", templateName: "text", labelName: "address2" },
        { fieldName: "address3", templateName: "text", labelName: "address3" },
        { fieldName: "locality", templateName: "text", labelName: "city" },
        { fieldName: "region", templateName: "text", labelName: "state", source: function(callback) {
            //get data here and call the callback function
        } },
        { fieldName: "postCode", templateName: "text", labelName: "zip" }
    ]);
    $.globalAddressExtend("CA", [
        { fieldName: "address1", templateName: "text", labelName: "address1" },
        { fieldName: "address2", templateName: "text", labelName: "address2" },
        { fieldName: "address3", templateName: "text", labelName: "address3" },
        { fieldName: "locality", templateName: "text", labelName: "city" },
        { fieldName: "region", templateName: "text", labelName: "province", source: function(callback) {
            //get data here and call the callback function
        } },
        { fieldName: "postCode", templateName: "text", labelName: "postalCode" }
    ]);
});
This would display, for example if Canada was selected, three address lines, the city, the province, and the postal code form elements in that order.

Then I would take my HTML page and attach the globalAddress plugin to the right elements.

<select id="country" name="country">
    <option value="" selected="selected">[Select a country]</option>
    <option value="US">United States</option>
    <option value="CA">Canada</option>
</select>
<div id="addressContainer"></div>
$(function() {
    $("#addressContainer").globalAddress("#country");   
});

The resulting HTML in the div#addressContainer when Canada is selected would look like the following.

<label for="address1">Address:</label>
<input id="address1" name="address1" type="text"/>
<label for="address2"></label>
<input id="address2" name="address2" type="text"/>
<label for="address3"></label>
<input id="address3" name="address3" type="text"/>
<label for="locality">City:</label>
<input id="locality" name="locality" type="text"/>
<label for="region">Province:</label>
<select id="region" name="region">
    <!-- <option> elements populated from data.source in the template -->
</select>
<label for="postCode">Postal Code:</label>
<input id="postCode" name="postCode" type="text"/>

That’s all there is to it. It can be extended by adding for labels, changing the data that gets supplied to the template function and defining new template functions to suit your needs. Let me know your questions, comments and suggestions. Also, should I make this an official jQuery plugin after it is refined?

No Comments