Showing posts with label jQuery. Show all posts
Showing posts with label jQuery. Show all posts

Tuesday, January 10, 2012

Save confirmation when leaving a web page

User story

A user opens a web form, fills in values in input elements, do not click "Save" button and clicks any navigation link. A confirmation message is shown: "There're unsaved changes. Do you wish to save them before navigating away?". If the user clicks "OK" - changes are saved and the desired page is opened. Clicking "Cancel" opens the destination page without saving user input.

Implementation

The idea is to have a JavaScript global object which is capable of tracking changes, can be called by custom JavaScript code (in order to notify the object about changes), which shows confirmation message in case of clicking navigation links and calls back to custom code in order let it do server requests and save data.

The first part is the prototype itself and global object instantiation (mind JQuery is required):

function ChangesTracker () {
this.confirmMessage =
"There're unsaved changes on the page. Do you wish to save them before navigating away?";
this.selectors = ["html body table tbody tr td.Toolbar a"];
this.ignoreSelectors = ["a#save", "a#remove"];
this.changedFlag = false;
this.enabled = false;
this.eventPublisher = {};
}

ChangesTracker.prototype.setChanged = function (state) {
this.changedFlag = state;
}

ChangesTracker.prototype.raiseOnLeave = function () {
if (this.enabled && this.changedFlag == true && this.showConfirmation()) {
$(this.eventPublisher).trigger("onLeaveWithChanges");
return true;
}
return false;
}

ChangesTracker.prototype.bindOnLeaveEvent = function (handler) {
$(this.eventPublisher).bind("onLeaveWithChanges", handler);
}

ChangesTracker.prototype.showConfirmation = function () {
return confirm(this.confirmMessage);
}

ChangesTracker.prototype.enable = function (selectors) {
var self = this;
if (selectors) this.selectors = selectors;
var query = $();

for (var i in this.selectors) {
query = query.add(this.selectors[i]);
}

for (var i in this.ignoreSelectors) {
query = query.not(this.ignoreSelectors[i]);
}

query.click(function () {
self.raiseOnLeave();
});

this.enabled = true;
}

ChangesTracker.prototype.disable = function () {
this.enabled = false;
}

var changesTracker = new ChangesTracker();

$(document).ready(function () {
changesTracker.enable();
});

The above code can be put to a separate script file and referenced on the pages which require changes tracking. Referencing the script will initialize a global variable changesTracker which is a ready to use obejct. The highlights about the object are:

<script type="text/javascript" src="JavaScripts/ChangesTracker.js"></script>
<script type="text/javascript" language="javascript">
changesTracker.bindOnLeaveEvent(function () {
$('form#fields').submit();
});

$(document).ready(function () {
$("#table.fields input").change(function () { changesTracker.setChanged(true) });
});
</script>

In our case we assume that there's a table with a number of <input> elements in it. The table is placed within a form with id fields. The 2 things we need to do are:
  • Notify the tracker about changes
  • Respond to tracker event and do the save

Known issues

1. Not working in IE6 and IE9 in comparability view mode
2. Form validation require minor amendments to the solution above (e.g. bindOnLeaveEvent may need args in order let the tracker know whether to leave the page)

Friday, December 30, 2011

HTML ComboBox control (and ASP.NET MVC sample)

Introduction

Combo box control in desktop applications is different from normal <select> HTML element in ability to type in arbitrary text:

Recently I've been searching for an easy option of implementing the above UI functionality in cross-browser manner within ASP.NET MVC application. After googling for some time I didn't find any solution according my requirements. On many forums I came across link to jQuery UI sample combo box widget which demos how Autocomplete widget can be extended to address similar requirements
The concept is simple, the widget extends specific <select> element by hiding it, adding input element and button on it's place and setting up Autocomplete widget to use <select> options as data source.
Please note that combo box widget is not included into jQuery UI bundle (as opposed to Autocomplete) and after you add to your page jQuery and jQuery UI scripts you'll also have to add custom combo box code snippet (script + CSS) from the above page.
After trying out the widget I've ended up with following issues:
  1. The control lets type in text but is not supposed to submit any custom input value
  2. The text typed is being validated against available options and if no corresponding text is found in <option> then user input is reverted
  3. CSS provided has cross-browser rendering issue

When I played a bit with the widget I recevied custom solution which is based on jQuery UI Autocomplete and Combobox sample:

  1. Text different from <select> options can be submitted to the server
  2. CSS was fixed to work in various browsers (tested in FF9, Chrome, IE 6, 7, 8, 9)

HTML output for the widget looks the following way:

As you can see the extended <select> is hidden, and input with name {select.name}Custom is rendered next to it.

Using the control

In order to use the widget on your page your need to:

  1. References jQuery
  2. Reference jQuery UI with auto complete included
  3. Reference ComboboxWidget.js script
  4. Include ComboBox.cs styles

All the above files can be downloaded from here. Includes in ASP.NET MVC views may look as goes bellow:

<script type="text/javascript" src="<%=Url.Content("~/JavaScripts/jquery-1.7.1_min.js")%>"></script>
<script type="text/javascript" src="<%=Url.Content("~/JavaScripts/jquery-ui-1.8.16.custom.min.js"/>
<link rel="stylesheet" type="text/css" href="<%=Url.Content("~/Style/ComboBox.css")%>"/>
<script type="text/javascript" src="<%=Url.Content("~/JavaScripts/ComboboxWidget.js")%>"></script>

Assume we have the following view model which is used in strongly typed view:
public class SampleViewModel
{
public string SelectedId { get; set; }
public string SelectedIdCustom { get; set; }
public Dictionary SelectOptions { get; set; }

public bool IsCustomValue
{
get
{
return SelectedIdCustom != null &&
!SelectOptions.Values.Contains(SelectedIdCustom);
}
}
}

Rendering <select> element and binding the widget in the view is a follows:

<%= Html.DropDownListFor(m => m.SelectedId, new SelectList(Model.SelectOption, "Key", "Value"))%>

$(document).ready(function () {
$("#SelectedId").combobox();
});

If the above element is placed in a form for action "Save" the controller for processing combo box values looks the following way:

public ViewResult Save(SampleViewModel viewModel)
{
GetSelectOptions(viewModel); // Fetch DB values and fill in SelectOptions dictionary

if (viewModel.IsCustomValue) // Is user typed in custom text
{
SaveNewValue(viewModel.SelectedIdCustom); // Do some actions with the value
}
else // a user has selected one of existing options
{
SaveExistingValue(viewModel.SelectedIdCustom); // Do some actions
}

return View(viewModel);
}