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?
After searching for a solution to use reCAPTCHA with ASP.NET MVC, I have come up with no way that works well with the MVC architecture. Armed with frustration and my OCD tendencies, I created RecaptchaMvc.
To get started you need a free public and private key from reCAPTCHA and the RecaptchaMvc Beta 1.
RecaptchaMvc comes with an HTML helper to render the appropriate markup for the reCAPTCHA service. The HTML helper requires an IRecaptchaModelState object that contains the public key and error code (more on this later). My suggestion would be to create a ViewPage with the generic typed Model like so.
1: using System.Web.Mvc;
2: using RecaptchaMvc;
3:
4: public class RecaptchaTest : viewPage<IRecaptchaModelState> {}
Now you can import the RecaptchaMvc framework to the page and use the HTML helper to render the appropriate markup.
1: <%@ Import Namespace="RecaptchaMvc" %>
2:
3: <%= Html.Recaptcha(ViewData.Model) % >
The HTML helper has three overloads. One accepts only the IRecaptchaModelState object. The other two accept either a dictionary or object that describe the options to be rendered for the reCAPTCHA service. The reCAPTCHA client API page shows the options that reCAPTCHA can use.
In your controller you would return the View with the appropriate IRecaptchaModelState. The framework comes with a RecaptchaModelState object that allows you to construct it with your public key and error code (more on this later).
1: using System.Web.Mvc;
2: using RecaptchaMvc;
3:
4: [AcceptVerbs(HttpVerbs.Get)]
5: public ActionResult RecaptchaTest() { 6: string publicKey = WebConfigurationManager.AppSettings["RecaptchaPublicKey"]
7: return View(new RecaptchaModelState(publicKey));
8: }
I store my public and private keys from reCAPTCHA in the web.config file, so I can easily change them depending if it is deployed in development or production. I use the AcceptVerbsAttribute to limit which requests use this action method. When the View renders it will have the appropriate IRecaptchaModelState in its ViewData.Model property for the HTML helper to use.
When the form is submitted to the server, we need to validate the response with reCAPTCHA. The framework includes two methods for just this task: TryValidateRecaptcha and ValidateRecaptcha. Both methods have three overloads. One that asks for the private key. This overload will use the DefaultValueProvider to retrieve the challenge and the response from the post. One asks for the private key and an IValueProvider to retrieve the challenge and the response from the provider. The last one accepts an IRecaptchaRequest object that specifies the private key, the client's remote IP, the challenge and the response. TryValidateRecaptcha will return an IRecaptchaResponse object that specifies whether the response was valid and an error code if validation failed. The ValidateRecaptcha method will throw a RecaptchaValidationException if validation fails. The RecaptchaValidationException contains an IRecaptchaResponse object. You can use both of these methods to validate the reCAPTCHA response and display the appropriate view to the user. If validation fails, you can return a view with an IRecaptchaModelState object that contains the public key and the error code from the IRecaptchaResponse.
1: using System.Web.Mvc;
2: using RecaptchaMvc;
3:
4: [AcceptVerbs(HttpVerbs.Post)]
5: public ActionResult RecaptchaTest(FormCollection form) { 6: string privateKey = WebConfigurationManager.AppSettings["RecaptchaPrivateKey"];
7: IRecaptchaResponse response = TryValidateRecaptcha(privateKey, form);
8: if(!response.IsValid) { 9: string publicKey = WebConfigrationManager.AppSettings["RecaptchaPublicKey"];
10: return View(new RecaptchaModelState(publicKey, response.ErrorCode));
11: }
12: return RedirectToAction("RecaptchaTestPassed"); 13: }
1: using System.Web.Mvc;
2: using RecaptchaMvc;
3:
4: [AcceptVerbs(HttpVerbs.Post)]
5: public ActionResult RecaptchaTest(FormCollection form) { 6: try { 7: string privateKey = WebConfigurationManager.AppSettings["RecaptchaPrivateKey"];
8: ValidateRecaptcha(privateKey, form);
9: }
10: catch(RecaptchaValidationException ex) { 11: string publicKey = WebConfigrationManager.AppSettings["RecaptchaPublicKey"];
12: return View(new RecaptchaModelState(publicKey, ex.Response.ErrorCode));
13: }
14: return RedirectToAction("RecaptchaTestPassed"); 15: }
Well there you have it. A solution to using reCAPTCHA in ASP.NET MVC without any tricky hacks. If you have any suggestions for improvement or find a bug, please let me know. I invite all constructive feedback, positive or negative.